diff --git a/src/libraries/Microsoft.Extensions.Options/ref/Microsoft.Extensions.Options.cs b/src/libraries/Microsoft.Extensions.Options/ref/Microsoft.Extensions.Options.cs index c6564969132635..70f856d1313cfb 100644 --- a/src/libraries/Microsoft.Extensions.Options/ref/Microsoft.Extensions.Options.cs +++ b/src/libraries/Microsoft.Extensions.Options/ref/Microsoft.Extensions.Options.cs @@ -171,6 +171,7 @@ public OptionsBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollectio public virtual Microsoft.Extensions.Options.OptionsBuilder PostConfigure(System.Action configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class { throw null; } public virtual Microsoft.Extensions.Options.OptionsBuilder PostConfigure(System.Action configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class where TDep4 : class { throw null; } public virtual Microsoft.Extensions.Options.OptionsBuilder PostConfigure(System.Action configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class where TDep4 : class where TDep5 : class { throw null; } + public virtual Microsoft.Extensions.Options.OptionsBuilder Validate<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TValidateOptions>() where TValidateOptions : class, Microsoft.Extensions.Options.IValidateOptions { throw null; } public virtual Microsoft.Extensions.Options.OptionsBuilder Validate(System.Func validation) { throw null; } public virtual Microsoft.Extensions.Options.OptionsBuilder Validate(System.Func validation, string failureMessage) { throw null; } public virtual Microsoft.Extensions.Options.OptionsBuilder Validate(System.Func validation) where TDep : notnull { throw null; } diff --git a/src/libraries/Microsoft.Extensions.Options/src/NamedValidateOptionsFilter.cs b/src/libraries/Microsoft.Extensions.Options/src/NamedValidateOptionsFilter.cs new file mode 100644 index 00000000000000..1377c191e4f8a8 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Options/src/NamedValidateOptionsFilter.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Options +{ + internal sealed class NamedValidateOptionsFilter : IValidateOptions + where TOptions : class + where TInner : IValidateOptions + { + private readonly string _name; + private readonly TInner _inner; + + internal NamedValidateOptionsFilter(string name, TInner inner) + { + ArgumentNullException.ThrowIfNull(inner); + + _name = name; + _inner = inner; + } + + public ValidateOptionsResult Validate(string? name, TOptions options) + { + if (name is null || name == _name) + { + return _inner.Validate(name, options); + } + + // ignored if not validating this instance + return ValidateOptionsResult.Skip; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Options/src/OptionsBuilder.cs b/src/libraries/Microsoft.Extensions.Options/src/OptionsBuilder.cs index 37704e947e9eb3..a007a45a0fb7ea 100644 --- a/src/libraries/Microsoft.Extensions.Options/src/OptionsBuilder.cs +++ b/src/libraries/Microsoft.Extensions.Options/src/OptionsBuilder.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.Options @@ -335,6 +336,24 @@ public virtual OptionsBuilder PostConfigure + /// Registers an type for an options type. + /// + /// The validation type. + /// The current . + /// + /// Validation is scoped to the options name associated with this builder. + /// Dependencies required by + /// are resolved from the service provider. + /// + public virtual OptionsBuilder Validate<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TValidateOptions>() + where TValidateOptions : class, IValidateOptions + { + Services.AddTransient>(sp => + new NamedValidateOptionsFilter(Name, ActivatorUtilities.GetServiceOrCreateInstance(sp))); + return this; + } + /// /// Registers a validation action for an options type using a default failure message. /// diff --git a/src/libraries/Microsoft.Extensions.Options/src/PACKAGE.md b/src/libraries/Microsoft.Extensions.Options/src/PACKAGE.md index ee0cc2ab6f99ee..0842fe91ac144a 100644 --- a/src/libraries/Microsoft.Extensions.Options/src/PACKAGE.md +++ b/src/libraries/Microsoft.Extensions.Options/src/PACKAGE.md @@ -58,12 +58,10 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); // Configuration to validate -builder.Services.Configure(builder.Configuration.GetSection( - MyConfigOptions.MyConfig)); - -// OPtions validation through the DI container -builder.Services.AddSingleton, MyConfigValidation>(); +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig)) + // Validate with custom IValidateOptions + .Validate(); var app = builder.Build(); @@ -134,13 +132,11 @@ public class MyConfigOptions public partial class MyConfigValidation : IValidateOptions { // Source generator will automatically provide the implementation of IValidateOptions - // Then you can add the validation to the DI Container using the following code: + // Then you can add the validation using the following code: // - // builder.Services.AddSingleton, MyConfigValidation>(); // builder.Services.AddOptions() // .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig)) - // .ValidateDataAnnotations(); + // .Validate(); } ``` 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..15be6497c82182 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,92 @@ public void ValidateOnStart_ConfigureBasedOnDataAnnotationRestrictions_Validatio Assert.NotNull(value); } + + [Fact] + public void ValidateWithValidatorType_ValidatesFailureCorrectly() + { + var services = new ServiceCollection(); + services.AddOptions() + .Configure(o => o.Boolean = false) + .Validate(); + + var sp = services.BuildServiceProvider(); + + var error = Assert.Throws(() => sp.GetRequiredService>().Value); + ValidateFailure(error, Options.DefaultName, 1, "Boolean != true"); + } + + [Fact] + public void ValidateWithValidatorType_ValidationSuccessful() + { + var services = new ServiceCollection(); + services.AddOptions() + .Configure(o => o.Boolean = true) + .Validate(); + + var sp = services.BuildServiceProvider(); + + var value = sp.GetRequiredService>().Value; + + Assert.NotNull(value); + } + + [Fact] + public void ValidateWithValidatorType_WithDependencies_ValidationSuccessful() + { + var services = new ServiceCollection(); + var dependency = new ObservableDependency(); + services.AddSingleton(dependency); + services.AddOptions() + .Validate(); + + var sp = services.BuildServiceProvider(); + + var value = sp.GetRequiredService>().Value; + + Assert.NotNull(value); + Assert.True(dependency.HasBeenCalled); + } + + private class FakeOptionsValidatorWithDependencies(ObservableDependency dependency) : IValidateOptions + { + public ValidateOptionsResult Validate(string? name, FakeOptions options) + { + dependency.Call(); + return ValidateOptionsResult.Success; + } + } + + private class ObservableDependency + { + public bool HasBeenCalled { get; private set; } + + public void Call() + { + HasBeenCalled = true; + } + } + + [Fact] + public void ValidateWithValidatorType_AreScopedToNamedOptions() + { + var services = new ServiceCollection(); + + services.AddOptions("unvalidated") + .Configure(o => o.Boolean = false); + + services.AddOptions("validated") + .Configure(o => o.Boolean = false) + .Validate(); + + var sp = services.BuildServiceProvider(); + var monitor = sp.GetRequiredService>(); + + var error = Assert.Throws(() => monitor.Get("validated")); + ValidateFailure(error, "validated", 1, "Boolean != true"); + + var unvalidated = monitor.Get("unvalidated"); + Assert.NotNull(unvalidated); + } } }