From 7d34044c2c9cf6b1fa45ff432455f65f5f001319 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:06:13 +0000 Subject: [PATCH 1/2] Initial plan From e11f8bc19cb7016d7574822735b48b4998ff3926 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 07:58:16 +0000 Subject: [PATCH 2/2] Wire up IServiceProvider to ValidationContext in DataAnnotations options validation Pass the root IServiceProvider to ValidationContext when validating DataAnnotations attributes on Options. This enables custom validation attributes to resolve services via validationContext.GetService() or validationContext.GetRequiredService(). Changes: - Add new public constructor DataAnnotationValidateOptions(name, serviceProvider) - Change ValidateDataAnnotations() to register using a factory that captures and passes IServiceProvider to the new constructor - Pass IServiceProvider through to all ValidationContext instances, including nested ValidateObjectMembers and ValidateEnumeratedItems calls - Update ref assembly with the new constructor - Add tests verifying service resolution works and fails correctly Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/b340196e-dcb9-42de-ad3c-eb10b9d9199c Co-authored-by: rosebyte <14963300+rosebyte@users.noreply.github.com> --- ...soft.Extensions.Options.DataAnnotations.cs | 2 + .../src/DataAnnotationValidateOptions.cs | 25 ++++++++-- ...OptionsBuilderDataAnnotationsExtensions.cs | 3 +- .../OptionsBuilderTest.cs | 48 +++++++++++++++++++ 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Options.DataAnnotations/ref/Microsoft.Extensions.Options.DataAnnotations.cs b/src/libraries/Microsoft.Extensions.Options.DataAnnotations/ref/Microsoft.Extensions.Options.DataAnnotations.cs index 874dd859fe83e8..90a7e45e3725c7 100644 --- a/src/libraries/Microsoft.Extensions.Options.DataAnnotations/ref/Microsoft.Extensions.Options.DataAnnotations.cs +++ b/src/libraries/Microsoft.Extensions.Options.DataAnnotations/ref/Microsoft.Extensions.Options.DataAnnotations.cs @@ -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; } } diff --git a/src/libraries/Microsoft.Extensions.Options.DataAnnotations/src/DataAnnotationValidateOptions.cs b/src/libraries/Microsoft.Extensions.Options.DataAnnotations/src/DataAnnotationValidateOptions.cs index 2b85690478eff2..4b9fad94bbe662 100644 --- a/src/libraries/Microsoft.Extensions.Options.DataAnnotations/src/DataAnnotationValidateOptions.cs +++ b/src/libraries/Microsoft.Extensions.Options.DataAnnotations/src/DataAnnotationValidateOptions.cs @@ -26,8 +26,21 @@ 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) + { + } + + /// + /// Initializes a new instance of . + /// + /// The name of the option. + /// An to be used for resolving services in . + [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; } /// @@ -35,6 +48,8 @@ public DataAnnotationValidateOptions(string? name) /// public string? Name { get; } + private readonly IServiceProvider? _serviceProvider; + /// /// Validates a specific named options instance (or all when is null). /// @@ -59,7 +74,7 @@ public ValidateOptionsResult Validate(string? name, TOptions options) HashSet? visited = null; List? 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; } @@ -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 results, ref List? errors, ref HashSet? visited) + private static bool TryValidateOptions(object options, string qualifiedName, List results, ref List? errors, ref HashSet? visited, IServiceProvider? serviceProvider) { Debug.Assert(options is not null); @@ -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(); @@ -114,7 +129,7 @@ private static bool TryValidateOptions(object options, string qualifiedName, Lis visited.Add(options); results ??= new List(); - 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() is not null) @@ -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; } } } diff --git a/src/libraries/Microsoft.Extensions.Options.DataAnnotations/src/OptionsBuilderDataAnnotationsExtensions.cs b/src/libraries/Microsoft.Extensions.Options.DataAnnotations/src/OptionsBuilderDataAnnotationsExtensions.cs index 55f8b33a70311d..b6e30ae80a9d90 100644 --- a/src/libraries/Microsoft.Extensions.Options.DataAnnotations/src/OptionsBuilderDataAnnotationsExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Options.DataAnnotations/src/OptionsBuilderDataAnnotationsExtensions.cs @@ -21,7 +21,8 @@ public static class OptionsBuilderDataAnnotationsExtensions " members may be trimmed.")] public static OptionsBuilder ValidateDataAnnotations<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] TOptions>(this OptionsBuilder optionsBuilder) where TOptions : class { - optionsBuilder.Services.AddSingleton>(new DataAnnotationValidateOptions(optionsBuilder.Name)); + optionsBuilder.Services.AddSingleton>( + sp => new DataAnnotationValidateOptions(optionsBuilder.Name, sp)); return optionsBuilder; } } diff --git a/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsBuilderTest.cs b/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsBuilderTest.cs index 342c02ad786691..0dd7e653c606a6 100644 --- a/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsBuilderTest.cs +++ b/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsBuilderTest.cs @@ -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) + { + 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() + .Configure(o => o.Required = "value") + .ValidateDataAnnotations(); + + var sp = services.BuildServiceProvider(); + + var value = sp.GetRequiredService>().Value; + Assert.NotNull(value); + } + + [Fact] + public void ValidateDataAnnotations_ServiceProviderFailsWhenServiceNotAvailableInValidationContext() + { + var services = new ServiceCollection(); + // Don't register SomeService + services.AddOptions() + .Configure(o => o.Required = "value") + .ValidateDataAnnotations(); + + var sp = services.BuildServiceProvider(); + + var error = Assert.Throws(() => sp.GetRequiredService>().Value); + Assert.Contains("SomeService not resolved correctly", error.Message); + } } }