Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public partial class DataAnnotationValidateOptions<[System.Diagnostics.CodeAnaly
{
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("The implementation of Validate method on this type will walk through all properties of the passed in options object, and its type cannot be statically analyzed so its members may be trimmed.")]
public DataAnnotationValidateOptions(string? name) { }
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("The implementation of Validate method on this type will walk through all properties of the passed in options object, and its type cannot be statically analyzed so its members may be trimmed.")]
public DataAnnotationValidateOptions(string? name, System.IServiceProvider? serviceProvider) { }
public string? Name { get { throw null; } }
public Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, TOptions options) { throw null; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,30 @@ public class DataAnnotationValidateOptions<[DynamicallyAccessedMembers(Dynamical
[RequiresUnreferencedCode("The implementation of Validate method on this type will walk through all properties of the passed in options object, and its type cannot be " +
"statically analyzed so its members may be trimmed.")]
public DataAnnotationValidateOptions(string? name)
: this(name, serviceProvider: null)
{
}

/// <summary>
/// Initializes a new instance of <see cref="DataAnnotationValidateOptions{TOptions}"/> .
/// </summary>
Comment on lines +33 to +35
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XML doc comment has an extra space before the period after the <see cref="DataAnnotationValidateOptions{TOptions}"/> reference; this shows up in generated docs and should be removed.

Copilot uses AI. Check for mistakes.
/// <param name="name">The name of the option.</param>
/// <param name="serviceProvider">An <see cref="IServiceProvider"/> to be used for resolving services in <see cref="ValidationContext"/>.</param>
[RequiresUnreferencedCode("The implementation of Validate method on this type will walk through all properties of the passed in options object, and its type cannot be " +
"statically analyzed so its members may be trimmed.")]
public DataAnnotationValidateOptions(string? name, IServiceProvider? serviceProvider)
{
Name = name;
_serviceProvider = serviceProvider;
}

/// <summary>
/// Gets the options name.
/// </summary>
public string? Name { get; }

private readonly IServiceProvider? _serviceProvider;

/// <summary>
/// Validates a specific named options instance (or all when <paramref name="name"/> is null).
/// </summary>
Expand All @@ -59,7 +74,7 @@ public ValidateOptionsResult Validate(string? name, TOptions options)
HashSet<object>? visited = null;
List<string>? errors = null;

if (TryValidateOptions(options, options.GetType().Name, validationResults, ref errors, ref visited))
if (TryValidateOptions(options, options.GetType().Name, validationResults, ref errors, ref visited, _serviceProvider))
{
return ValidateOptionsResult.Success;
}
Expand All @@ -71,7 +86,7 @@ public ValidateOptionsResult Validate(string? name, TOptions options)

[RequiresUnreferencedCode("This method on this type will walk through all properties of the passed in options object, and its type cannot be " +
"statically analyzed so its members may be trimmed.")]
private static bool TryValidateOptions(object options, string qualifiedName, List<ValidationResult> results, ref List<string>? errors, ref HashSet<object>? visited)
private static bool TryValidateOptions(object options, string qualifiedName, List<ValidationResult> results, ref List<string>? errors, ref HashSet<object>? visited, IServiceProvider? serviceProvider)
{
Debug.Assert(options is not null);

Expand All @@ -82,7 +97,7 @@ private static bool TryValidateOptions(object options, string qualifiedName, Lis

results.Clear();

bool res = Validator.TryValidateObject(options, new ValidationContext(options), results, validateAllProperties: true);
bool res = Validator.TryValidateObject(options, new ValidationContext(options, serviceProvider, null), results, validateAllProperties: true);
if (!res)
{
errors ??= new List<string>();
Expand Down Expand Up @@ -114,7 +129,7 @@ private static bool TryValidateOptions(object options, string qualifiedName, Lis
visited.Add(options);

results ??= new List<ValidationResult>();
res = TryValidateOptions(value, $"{qualifiedName}.{propertyInfo.Name}", results, ref errors, ref visited) && res;
res = TryValidateOptions(value, $"{qualifiedName}.{propertyInfo.Name}", results, ref errors, ref visited, serviceProvider) && res;
}
else if (value is IEnumerable enumerable &&
propertyInfo.GetCustomAttribute<ValidateEnumeratedItemsAttribute>() is not null)
Expand All @@ -126,7 +141,7 @@ private static bool TryValidateOptions(object options, string qualifiedName, Lis
int index = 0;
foreach (object item in enumerable)
{
res = TryValidateOptions(item, $"{qualifiedName}.{propertyInfo.Name}[{index++}]", results, ref errors, ref visited) && res;
res = TryValidateOptions(item, $"{qualifiedName}.{propertyInfo.Name}[{index++}]", results, ref errors, ref visited, serviceProvider) && res;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public static class OptionsBuilderDataAnnotationsExtensions
" members may be trimmed.")]
public static OptionsBuilder<TOptions> ValidateDataAnnotations<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] TOptions>(this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class
{
optionsBuilder.Services.AddSingleton<IValidateOptions<TOptions>>(new DataAnnotationValidateOptions<TOptions>(optionsBuilder.Name));
optionsBuilder.Services.AddSingleton<IValidateOptions<TOptions>>(
sp => new DataAnnotationValidateOptions<TOptions>(optionsBuilder.Name, sp));
Comment on lines +24 to +25
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The singleton factory lambda closes over the entire optionsBuilder instance. Capture optionsBuilder.Name into a local variable first and close over the string instead, to avoid retaining the builder object longer than necessary and to make the registration behavior clearer.

Suggested change
optionsBuilder.Services.AddSingleton<IValidateOptions<TOptions>>(
sp => new DataAnnotationValidateOptions<TOptions>(optionsBuilder.Name, sp));
string? name = optionsBuilder.Name;
optionsBuilder.Services.AddSingleton<IValidateOptions<TOptions>>(
sp => new DataAnnotationValidateOptions<TOptions>(name, sp));

Copilot uses AI. Check for mistakes.
return optionsBuilder;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -814,5 +814,53 @@ public void ValidateOnStart_ConfigureBasedOnDataAnnotationRestrictions_Validatio

Assert.NotNull(value);
}

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class RequiresServiceAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ValidationAttribute.IsValid is nullability-annotated in the BCL; to avoid signature mismatches/warnings and to correctly express the contract, this override should use object? for value and return ValidationResult?.

Suggested change
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)

Copilot uses AI. Check for mistakes.
{
var service = validationContext.GetService(typeof(SomeService));
return service is SomeService someService && someService.Stuff == "stuff"
? ValidationResult.Success
: new ValidationResult("SomeService not resolved correctly");
}
}

private class AnnotatedOptionsWithServiceDependency
{
[RequiresService]
public string Required { get; set; }
}

[Fact]
public void ValidateDataAnnotations_ServiceProviderIsAvailableInValidationContext()
{
var services = new ServiceCollection();
services.AddSingleton(new SomeService("stuff"));
services.AddOptions<AnnotatedOptionsWithServiceDependency>()
.Configure(o => o.Required = "value")
.ValidateDataAnnotations();

var sp = services.BuildServiceProvider();

var value = sp.GetRequiredService<IOptions<AnnotatedOptionsWithServiceDependency>>().Value;
Assert.NotNull(value);
}

[Fact]
public void ValidateDataAnnotations_ServiceProviderFailsWhenServiceNotAvailableInValidationContext()
{
var services = new ServiceCollection();
// Don't register SomeService
services.AddOptions<AnnotatedOptionsWithServiceDependency>()
.Configure(o => o.Required = "value")
.ValidateDataAnnotations();

var sp = services.BuildServiceProvider();

var error = Assert.Throws<OptionsValidationException>(() => sp.GetRequiredService<IOptions<AnnotatedOptionsWithServiceDependency>>().Value);
Assert.Contains("SomeService not resolved correctly", error.Message);
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test asserts on OptionsValidationException.Message, which is derived formatting over Failures and can be brittle if message formatting changes. Prefer asserting on error.Failures (or using the existing ValidateFailure<TOptions> helper in this file) to validate the exact failure(s) without depending on Message formatting.

Suggested change
Assert.Contains("SomeService not resolved correctly", error.Message);
Assert.Contains("SomeService not resolved correctly", error.Failures);

Copilot uses AI. Check for mistakes.
}
}
}
Loading