From dfcde1b190051c3d62a601a834ccee54c12d6089 Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Tue, 28 Aug 2018 15:46:17 -0700 Subject: [PATCH] Add DataAnnotations based validation (#272) --- Options.sln | 7 ++ build/dependencies.props | 1 + .../ConfigurationChangeTokenSource.cs | 3 + ...ons.Options.ConfigurationExtensions.csproj | 2 +- .../DataAnnotationValidateOptions.cs | 61 ++++++++++ ....Extensions.Options.DataAnnotations.csproj | 20 ++++ ...OptionsBuilderDataAnnotationsExtensions.cs | 25 ++++ .../baseline.netcore.json | 2 + .../ConfigureNamedOptions.cs | 102 +++++++++++++++- src/Microsoft.Extensions.Options/IOptions.cs | 2 +- .../Microsoft.Extensions.Options.csproj | 3 +- src/Microsoft.Extensions.Options/Options.cs | 3 + .../OptionsBuilder.cs | 103 ++++++++++++++++- .../OptionsFactory.cs | 3 + .../OptionsManager.cs | 8 +- .../OptionsMonitor.cs | 3 + .../OptionsValidationException.cs | 6 + .../PostConfigureOptions.cs | 93 ++++++++++++++- .../ValidateOptions.cs | 12 ++ .../Microsoft.Extensions.Options.Test.csproj | 1 + .../OptionsBuilderTest.cs | 109 +++++++++++++++++- 21 files changed, 551 insertions(+), 18 deletions(-) create mode 100644 src/Microsoft.Extensions.Options.DataAnnotations/DataAnnotationValidateOptions.cs create mode 100644 src/Microsoft.Extensions.Options.DataAnnotations/Microsoft.Extensions.Options.DataAnnotations.csproj create mode 100644 src/Microsoft.Extensions.Options.DataAnnotations/OptionsBuilderDataAnnotationsExtensions.cs create mode 100644 src/Microsoft.Extensions.Options.DataAnnotations/baseline.netcore.json diff --git a/Options.sln b/Options.sln index ce4ce7216f5..92d6737e7c7 100644 --- a/Options.sln +++ b/Options.sln @@ -37,6 +37,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution version.xml = version.xml EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Options.DataAnnotations", "src\Microsoft.Extensions.Options.DataAnnotations\Microsoft.Extensions.Options.DataAnnotations.csproj", "{D0EB1487-D9E9-4C58-A907-BCD595993251}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -55,6 +57,10 @@ Global {BA4EF3CE-1829-4E0E-8281-BD503FF8A682}.Debug|Any CPU.Build.0 = Debug|Any CPU {BA4EF3CE-1829-4E0E-8281-BD503FF8A682}.Release|Any CPU.ActiveCfg = Release|Any CPU {BA4EF3CE-1829-4E0E-8281-BD503FF8A682}.Release|Any CPU.Build.0 = Release|Any CPU + {D0EB1487-D9E9-4C58-A907-BCD595993251}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0EB1487-D9E9-4C58-A907-BCD595993251}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0EB1487-D9E9-4C58-A907-BCD595993251}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0EB1487-D9E9-4C58-A907-BCD595993251}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -63,6 +69,7 @@ Global {16BADE2F-1184-4518-8A70-B68A19D0805B} = {0A4664A0-CB48-4338-A6B7-02E28DF62CBA} {6ACF4BAB-2F09-4DA6-B273-27E4282865EB} = {10221BD9-FD19-4809-B680-7628CB87926B} {BA4EF3CE-1829-4E0E-8281-BD503FF8A682} = {10221BD9-FD19-4809-B680-7628CB87926B} + {D0EB1487-D9E9-4C58-A907-BCD595993251} = {10221BD9-FD19-4809-B680-7628CB87926B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A4D0BBDB-82DD-4B7E-981F-E6B90F3850B6} diff --git a/build/dependencies.props b/build/dependencies.props index 299dca8da49..19f487de01c 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -13,6 +13,7 @@ 2.1.2 2.2.0-preview1-26618-02 15.6.1 + 4.6.0-preview1-26617-01 2.0.3 2.3.1 2.4.0 diff --git a/src/Microsoft.Extensions.Options.ConfigurationExtensions/ConfigurationChangeTokenSource.cs b/src/Microsoft.Extensions.Options.ConfigurationExtensions/ConfigurationChangeTokenSource.cs index 29a2d382307..b02aed0832e 100644 --- a/src/Microsoft.Extensions.Options.ConfigurationExtensions/ConfigurationChangeTokenSource.cs +++ b/src/Microsoft.Extensions.Options.ConfigurationExtensions/ConfigurationChangeTokenSource.cs @@ -37,6 +37,9 @@ public ConfigurationChangeTokenSource(string name, IConfiguration config) Name = name ?? Options.DefaultName; } + /// + /// The name of the option instance being changed. + /// public string Name { get; } /// diff --git a/src/Microsoft.Extensions.Options.ConfigurationExtensions/Microsoft.Extensions.Options.ConfigurationExtensions.csproj b/src/Microsoft.Extensions.Options.ConfigurationExtensions/Microsoft.Extensions.Options.ConfigurationExtensions.csproj index 283f524d902..8d68cb58a13 100644 --- a/src/Microsoft.Extensions.Options.ConfigurationExtensions/Microsoft.Extensions.Options.ConfigurationExtensions.csproj +++ b/src/Microsoft.Extensions.Options.ConfigurationExtensions/Microsoft.Extensions.Options.ConfigurationExtensions.csproj @@ -3,7 +3,7 @@ Provides additional configuration specific functionality related to Options. netstandard2.0 - $(NoWarn);CS1591 + $(NoWarn) true aspnetcore;configuration;options diff --git a/src/Microsoft.Extensions.Options.DataAnnotations/DataAnnotationValidateOptions.cs b/src/Microsoft.Extensions.Options.DataAnnotations/DataAnnotationValidateOptions.cs new file mode 100644 index 00000000000..4dd784d6723 --- /dev/null +++ b/src/Microsoft.Extensions.Options.DataAnnotations/DataAnnotationValidateOptions.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace Microsoft.Extensions.Options +{ + /// + /// Implementation of that uses DataAnnotation's for validation. + /// + /// The instance being validated. + public class DataAnnotationValidateOptions : IValidateOptions where TOptions : class + { + /// + /// Constructor. + /// + /// + public DataAnnotationValidateOptions(string name) + { + Name = name; + } + + /// + /// The options name. + /// + public string Name { get; } + + /// + /// Validates a specific named options instance (or all when name is null). + /// + /// The name of the options instance being validated. + /// The options instance. + /// The result. + public ValidateOptionsResult Validate(string name, TOptions options) + { + // Null name is used to configure all named options. + if (Name == null || name == Name) + { + var validationResults = new List(); + if (Validator.TryValidateObject(options, + new ValidationContext(options, serviceProvider: null, items: null), + validationResults, + validateAllProperties: true)) + { + return ValidateOptionsResult.Success; + } + + return ValidateOptionsResult.Fail(String.Join(Environment.NewLine, + validationResults.Select(r => "DataAnnotation validation failed for members " + + String.Join(", ", r.MemberNames) + + " with the error '" + r.ErrorMessage + "'."))); + } + + // Ignored if not validating this instance. + return ValidateOptionsResult.Skip; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options.DataAnnotations/Microsoft.Extensions.Options.DataAnnotations.csproj b/src/Microsoft.Extensions.Options.DataAnnotations/Microsoft.Extensions.Options.DataAnnotations.csproj new file mode 100644 index 00000000000..c15084b4959 --- /dev/null +++ b/src/Microsoft.Extensions.Options.DataAnnotations/Microsoft.Extensions.Options.DataAnnotations.csproj @@ -0,0 +1,20 @@ + + + + Provides additional DataAnnotations specific functionality related to Options. + netstandard2.0 + $(NoWarn) + true + aspnetcore;validation;options + + + + + + + + + + + + diff --git a/src/Microsoft.Extensions.Options.DataAnnotations/OptionsBuilderDataAnnotationsExtensions.cs b/src/Microsoft.Extensions.Options.DataAnnotations/OptionsBuilderDataAnnotationsExtensions.cs new file mode 100644 index 00000000000..1df05d4a330 --- /dev/null +++ b/src/Microsoft.Extensions.Options.DataAnnotations/OptionsBuilderDataAnnotationsExtensions.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extension methods for adding configuration related options services to the DI container via . + /// + public static class OptionsBuilderDataAnnotationsExtensions + { + /// + /// Register this options instance for validation of its DataAnnotations. + /// + /// The options type to be configured. + /// The options builder to add the services to. + /// The so that additional calls can be chained. + public static OptionsBuilder ValidateDataAnnotations(this OptionsBuilder optionsBuilder) where TOptions : class + { + optionsBuilder.Services.AddSingleton>(new DataAnnotationValidateOptions(optionsBuilder.Name)); + return optionsBuilder; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options.DataAnnotations/baseline.netcore.json b/src/Microsoft.Extensions.Options.DataAnnotations/baseline.netcore.json new file mode 100644 index 00000000000..7a73a41bfdf --- /dev/null +++ b/src/Microsoft.Extensions.Options.DataAnnotations/baseline.netcore.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options/ConfigureNamedOptions.cs b/src/Microsoft.Extensions.Options/ConfigureNamedOptions.cs index e860d38d596..702be8181ff 100644 --- a/src/Microsoft.Extensions.Options/ConfigureNamedOptions.cs +++ b/src/Microsoft.Extensions.Options/ConfigureNamedOptions.cs @@ -35,8 +35,8 @@ public ConfigureNamedOptions(string name, Action action) /// /// Invokes the registered configure Action if the name matches. /// - /// - /// + /// The name of the options instance being configured. + /// The options instance to configure. public virtual void Configure(string name, TOptions options) { if (options == null) @@ -51,6 +51,10 @@ public virtual void Configure(string name, TOptions options) } } + /// + /// Invoked to configure a TOptions instance with the . + /// + /// The options instance to configure. public void Configure(TOptions options) => Configure(Options.DefaultName, options); } @@ -86,8 +90,16 @@ public ConfigureNamedOptions(string name, TDep dependency, Action public Action Action { get; } + /// + /// The dependency. + /// public TDep Dependency { get; } + /// + /// Invokes the registered configure Action if the name matches. + /// + /// The name of the options instance being configured. + /// The options instance to configure. public virtual void Configure(string name, TOptions options) { if (options == null) @@ -102,6 +114,10 @@ public virtual void Configure(string name, TOptions options) } } + /// + /// Invoked to configure a TOptions instance with the . + /// + /// The options instance to configure. public void Configure(TOptions options) => Configure(Options.DefaultName, options); } @@ -141,10 +157,21 @@ public ConfigureNamedOptions(string name, TDep1 dependency, TDep2 dependency2, A /// public Action Action { get; } + /// + /// The first dependency. + /// public TDep1 Dependency1 { get; } + /// + /// The second dependency. + /// public TDep2 Dependency2 { get; } + /// + /// Invokes the registered configure Action if the name matches. + /// + /// The name of the options instance being configured. + /// The options instance to configure. public virtual void Configure(string name, TOptions options) { if (options == null) @@ -159,6 +186,10 @@ public virtual void Configure(string name, TOptions options) } } + /// + /// Invoked to configure a TOptions instance with the . + /// + /// The options instance to configure. public void Configure(TOptions options) => Configure(Options.DefaultName, options); } @@ -202,13 +233,26 @@ public ConfigureNamedOptions(string name, TDep1 dependency, TDep2 dependency2, T /// public Action Action { get; } + /// + /// The first dependency. + /// public TDep1 Dependency1 { get; } + /// + /// The second dependency. + /// public TDep2 Dependency2 { get; } + /// + /// The third dependency. + /// public TDep3 Dependency3 { get; } - + /// + /// Invokes the registered configure Action if the name matches. + /// + /// The name of the options instance being configured. + /// The options instance to configure. public virtual void Configure(string name, TOptions options) { if (options == null) @@ -223,6 +267,10 @@ public virtual void Configure(string name, TOptions options) } } + /// + /// Invoked to configure a TOptions instance with the . + /// + /// The options instance to configure. public void Configure(TOptions options) => Configure(Options.DefaultName, options); } @@ -270,15 +318,31 @@ public ConfigureNamedOptions(string name, TDep1 dependency1, TDep2 dependency2, /// public Action Action { get; } + /// + /// The first dependency. + /// public TDep1 Dependency1 { get; } + /// + /// The second dependency. + /// public TDep2 Dependency2 { get; } + /// + /// The third dependency. + /// public TDep3 Dependency3 { get; } + /// + /// The fourth dependency. + /// public TDep4 Dependency4 { get; } - + /// + /// Invokes the registered configure Action if the name matches. + /// + /// The name of the options instance being configured. + /// The options instance to configure. public virtual void Configure(string name, TOptions options) { if (options == null) @@ -293,6 +357,10 @@ public virtual void Configure(string name, TOptions options) } } + /// + /// Invoked to configure a TOptions instance with the . + /// + /// The options instance to configure. public void Configure(TOptions options) => Configure(Options.DefaultName, options); } @@ -344,17 +412,36 @@ public ConfigureNamedOptions(string name, TDep1 dependency1, TDep2 dependency2, /// public Action Action { get; } + /// + /// The first dependency. + /// public TDep1 Dependency1 { get; } + /// + /// The second dependency. + /// public TDep2 Dependency2 { get; } + /// + /// The third dependency. + /// public TDep3 Dependency3 { get; } + /// + /// The fourth dependency. + /// public TDep4 Dependency4 { get; } + /// + /// The fifth dependency. + /// public TDep5 Dependency5 { get; } - + /// + /// Invokes the registered configure Action if the name matches. + /// + /// The name of the options instance being configured. + /// The options instance to configure. public virtual void Configure(string name, TOptions options) { if (options == null) @@ -369,7 +456,10 @@ public virtual void Configure(string name, TOptions options) } } + /// + /// Invoked to configure a TOptions instance with the . + /// + /// The options instance to configure. public void Configure(TOptions options) => Configure(Options.DefaultName, options); } - } \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options/IOptions.cs b/src/Microsoft.Extensions.Options/IOptions.cs index 2b1970383bc..81420a660e5 100644 --- a/src/Microsoft.Extensions.Options/IOptions.cs +++ b/src/Microsoft.Extensions.Options/IOptions.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.Options public interface IOptions where TOptions : class, new() { /// - /// The default configured TOptions instance, equivalent to Get(string.Empty). + /// The default configured TOptions instance /// TOptions Value { get; } } diff --git a/src/Microsoft.Extensions.Options/Microsoft.Extensions.Options.csproj b/src/Microsoft.Extensions.Options/Microsoft.Extensions.Options.csproj index 71c6645449e..c513e455d83 100644 --- a/src/Microsoft.Extensions.Options/Microsoft.Extensions.Options.csproj +++ b/src/Microsoft.Extensions.Options/Microsoft.Extensions.Options.csproj @@ -3,7 +3,7 @@ Provides a strongly typed way of specifying and accessing settings using dependency injection. netstandard2.0 - $(NoWarn);CS1591 + $(NoWarn) true aspnetcore;options @@ -11,6 +11,7 @@ + diff --git a/src/Microsoft.Extensions.Options/Options.cs b/src/Microsoft.Extensions.Options/Options.cs index 1c33063c6a5..68d56be9d0d 100644 --- a/src/Microsoft.Extensions.Options/Options.cs +++ b/src/Microsoft.Extensions.Options/Options.cs @@ -8,6 +8,9 @@ namespace Microsoft.Extensions.Options /// public static class Options { + /// + /// The default name used for options instances: "". + /// public static readonly string DefaultName = string.Empty; /// diff --git a/src/Microsoft.Extensions.Options/OptionsBuilder.cs b/src/Microsoft.Extensions.Options/OptionsBuilder.cs index 771603a635b..c87b40e7d62 100644 --- a/src/Microsoft.Extensions.Options/OptionsBuilder.cs +++ b/src/Microsoft.Extensions.Options/OptionsBuilder.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.Options @@ -44,6 +43,7 @@ public OptionsBuilder(IServiceCollection services, string name) /// Note: These are run before all . /// /// The action used to configure the options. + /// The current OptionsBuilder. public virtual OptionsBuilder Configure(Action configureOptions) { if (configureOptions == null) @@ -55,6 +55,13 @@ public virtual OptionsBuilder Configure(Action configureOpti return this; } + /// + /// Registers an action used to configure a particular type of options. + /// Note: These are run before all . + /// + /// A dependency used by the action. + /// The action used to configure the options. + /// The current OptionsBuilder. public virtual OptionsBuilder Configure(Action configureOptions) where TDep : class { @@ -68,6 +75,14 @@ public virtual OptionsBuilder Configure(Action c return this; } + /// + /// Registers an action used to configure a particular type of options. + /// Note: These are run before all . + /// + /// The first dependency used by the action. + /// The second dependency used by the action. + /// The action used to configure the options. + /// The current OptionsBuilder. public virtual OptionsBuilder Configure(Action configureOptions) where TDep1 : class where TDep2 : class @@ -82,6 +97,15 @@ public virtual OptionsBuilder Configure(Action + /// Registers an action used to configure a particular type of options. + /// Note: These are run before all . + /// + /// The first dependency used by the action. + /// The second dependency used by the action. + /// The third dependency used by the action. + /// The action used to configure the options. + /// The current OptionsBuilder. public virtual OptionsBuilder Configure(Action configureOptions) where TDep1 : class where TDep2 : class @@ -102,6 +126,16 @@ public virtual OptionsBuilder Configure(Action + /// Registers an action used to configure a particular type of options. + /// Note: These are run before all . + /// + /// The first dependency used by the action. + /// The second dependency used by the action. + /// The third dependency used by the action. + /// The fourth dependency used by the action. + /// The action used to configure the options. + /// The current OptionsBuilder. public virtual OptionsBuilder Configure(Action configureOptions) where TDep1 : class where TDep2 : class @@ -124,6 +158,17 @@ public virtual OptionsBuilder Configure(Ac return this; } + /// + /// Registers an action used to configure a particular type of options. + /// Note: These are run before all . + /// + /// The first dependency used by the action. + /// The second dependency used by the action. + /// The third dependency used by the action. + /// The fourth dependency used by the action. + /// The fifth dependency used by the action. + /// The action used to configure the options. + /// The current OptionsBuilder. public virtual OptionsBuilder Configure(Action configureOptions) where TDep1 : class where TDep2 : class @@ -164,6 +209,13 @@ public virtual OptionsBuilder PostConfigure(Action configure return this; } + /// + /// Registers an action used to post configure a particular type of options. + /// Note: These are run before after . + /// + /// The dependency used by the action. + /// The action used to configure the options. + /// The current OptionsBuilder. public virtual OptionsBuilder PostConfigure(Action configureOptions) where TDep : class { @@ -177,6 +229,14 @@ public virtual OptionsBuilder PostConfigure(Action + /// Registers an action used to post configure a particular type of options. + /// Note: These are run before after . + /// + /// The first dependency used by the action. + /// The second dependency used by the action. + /// The action used to configure the options. + /// The current OptionsBuilder. public virtual OptionsBuilder PostConfigure(Action configureOptions) where TDep1 : class where TDep2 : class @@ -191,6 +251,15 @@ public virtual OptionsBuilder PostConfigure(Action + /// Registers an action used to post configure a particular type of options. + /// Note: These are run before after . + /// + /// The first dependency used by the action. + /// The second dependency used by the action. + /// The third dependency used by the action. + /// The action used to configure the options. + /// The current OptionsBuilder. public virtual OptionsBuilder PostConfigure(Action configureOptions) where TDep1 : class where TDep2 : class @@ -211,6 +280,16 @@ public virtual OptionsBuilder PostConfigure(Actio return this; } + /// + /// Registers an action used to post configure a particular type of options. + /// Note: These are run before after . + /// + /// The first dependency used by the action. + /// The second dependency used by the action. + /// The third dependency used by the action. + /// The fourth dependency used by the action. + /// The action used to configure the options. + /// The current OptionsBuilder. public virtual OptionsBuilder PostConfigure(Action configureOptions) where TDep1 : class where TDep2 : class @@ -233,6 +312,17 @@ public virtual OptionsBuilder PostConfigure + /// Registers an action used to post configure a particular type of options. + /// Note: These are run before after . + /// + /// The first dependency used by the action. + /// The second dependency used by the action. + /// The third dependency used by the action. + /// The fourth dependency used by the action. + /// The fifth dependency used by the action. + /// The action used to configure the options. + /// The current OptionsBuilder. public virtual OptionsBuilder PostConfigure(Action configureOptions) where TDep1 : class where TDep2 : class @@ -257,9 +347,20 @@ public virtual OptionsBuilder PostConfigure + /// Register a validation action for an options type using a default failure message.. + /// + /// The validation function. + /// The current OptionsBuilder. public virtual OptionsBuilder Validate(Func validation) => Validate(validation: validation, failureMessage: "A validation error has occured."); + /// + /// Register a validation action for an options type. + /// + /// The validation function. + /// The failure message to use when validation fails. + /// The current OptionsBuilder. public virtual OptionsBuilder Validate(Func validation, string failureMessage) { if (validation == null) diff --git a/src/Microsoft.Extensions.Options/OptionsFactory.cs b/src/Microsoft.Extensions.Options/OptionsFactory.cs index 6d8a79fef10..486ae8f9832 100644 --- a/src/Microsoft.Extensions.Options/OptionsFactory.cs +++ b/src/Microsoft.Extensions.Options/OptionsFactory.cs @@ -36,6 +36,9 @@ public OptionsFactory(IEnumerable> setups, IEnumerab _validations = validations; } + /// + /// Returns a configured TOptions instance with the given name. + /// public TOptions Create(string name) { var options = new TOptions(); diff --git a/src/Microsoft.Extensions.Options/OptionsManager.cs b/src/Microsoft.Extensions.Options/OptionsManager.cs index c1b2f09a4db..012438683c7 100644 --- a/src/Microsoft.Extensions.Options/OptionsManager.cs +++ b/src/Microsoft.Extensions.Options/OptionsManager.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; - namespace Microsoft.Extensions.Options { /// @@ -23,6 +21,9 @@ public OptionsManager(IOptionsFactory factory) _factory = factory; } + /// + /// The default configured TOptions instance, equivalent to Get(Options.DefaultName). + /// public TOptions Value { get @@ -31,6 +32,9 @@ public TOptions Value } } + /// + /// Returns a configured TOptions instance with the given name. + /// public virtual TOptions Get(string name) { name = name ?? Options.DefaultName; diff --git a/src/Microsoft.Extensions.Options/OptionsMonitor.cs b/src/Microsoft.Extensions.Options/OptionsMonitor.cs index dbd678df698..6d6d1387c2f 100644 --- a/src/Microsoft.Extensions.Options/OptionsMonitor.cs +++ b/src/Microsoft.Extensions.Options/OptionsMonitor.cs @@ -58,6 +58,9 @@ public TOptions CurrentValue get => Get(Options.DefaultName); } + /// + /// Returns a configured TOptions instance with the given name. + /// public virtual TOptions Get(string name) { name = name ?? Options.DefaultName; diff --git a/src/Microsoft.Extensions.Options/OptionsValidationException.cs b/src/Microsoft.Extensions.Options/OptionsValidationException.cs index 8d9f53f5757..a02f2886577 100644 --- a/src/Microsoft.Extensions.Options/OptionsValidationException.cs +++ b/src/Microsoft.Extensions.Options/OptionsValidationException.cs @@ -24,8 +24,14 @@ public OptionsValidationException(string optionsName, Type optionsType, IEnumera OptionsName = optionsName ?? throw new ArgumentNullException(nameof(optionsName)); } + /// + /// The name of the options instance that failed. + /// public string OptionsName { get; } + /// + /// The type of the options that failed. + /// public Type OptionsType { get; } /// diff --git a/src/Microsoft.Extensions.Options/PostConfigureOptions.cs b/src/Microsoft.Extensions.Options/PostConfigureOptions.cs index fe261e95964..679b3527884 100644 --- a/src/Microsoft.Extensions.Options/PostConfigureOptions.cs +++ b/src/Microsoft.Extensions.Options/PostConfigureOptions.cs @@ -84,8 +84,16 @@ public PostConfigureOptions(string name, TDep dependency, Action /// public Action Action { get; } + /// + /// The dependency. + /// public TDep Dependency { get; } + /// + /// Invoked to configure a TOptions instance. + /// + /// The name of the options instance being configured. + /// The options instance to configured. public virtual void PostConfigure(string name, TOptions options) { if (options == null) @@ -100,6 +108,10 @@ public virtual void PostConfigure(string name, TOptions options) } } + /// + /// Invoked to configure a TOptions instance using the . + /// + /// The options instance to configured. public void PostConfigure(TOptions options) => PostConfigure(Options.DefaultName, options); } @@ -139,10 +151,21 @@ public PostConfigureOptions(string name, TDep1 dependency, TDep2 dependency2, Ac /// public Action Action { get; } + /// + /// The first dependency. + /// public TDep1 Dependency1 { get; } + /// + /// The second dependency. + /// public TDep2 Dependency2 { get; } + /// + /// Invoked to configure a TOptions instance. + /// + /// The name of the options instance being configured. + /// The options instance to configured. public virtual void PostConfigure(string name, TOptions options) { if (options == null) @@ -157,6 +180,10 @@ public virtual void PostConfigure(string name, TOptions options) } } + /// + /// Invoked to configure a TOptions instance using the . + /// + /// The options instance to configured. public void PostConfigure(TOptions options) => PostConfigure(Options.DefaultName, options); } @@ -200,13 +227,26 @@ public PostConfigureOptions(string name, TDep1 dependency, TDep2 dependency2, TD /// public Action Action { get; } + /// + /// The first dependency. + /// public TDep1 Dependency1 { get; } + /// + /// The second dependency. + /// public TDep2 Dependency2 { get; } + /// + /// The third dependency. + /// public TDep3 Dependency3 { get; } - + /// + /// Invoked to configure a TOptions instance. + /// + /// The name of the options instance being configured. + /// The options instance to configured. public virtual void PostConfigure(string name, TOptions options) { if (options == null) @@ -221,6 +261,10 @@ public virtual void PostConfigure(string name, TOptions options) } } + /// + /// Invoked to configure a TOptions instance using the . + /// + /// The options instance to configured. public void PostConfigure(TOptions options) => PostConfigure(Options.DefaultName, options); } @@ -268,15 +312,31 @@ public PostConfigureOptions(string name, TDep1 dependency1, TDep2 dependency2, T /// public Action Action { get; } + /// + /// The first dependency. + /// public TDep1 Dependency1 { get; } + /// + /// The second dependency. + /// public TDep2 Dependency2 { get; } + /// + /// The third dependency. + /// public TDep3 Dependency3 { get; } + /// + /// The fourth dependency. + /// public TDep4 Dependency4 { get; } - + /// + /// Invoked to configure a TOptions instance. + /// + /// The name of the options instance being configured. + /// The options instance to configured. public virtual void PostConfigure(string name, TOptions options) { if (options == null) @@ -291,6 +351,10 @@ public virtual void PostConfigure(string name, TOptions options) } } + /// + /// Invoked to configure a TOptions instance using the . + /// + /// The options instance to configured. public void PostConfigure(TOptions options) => PostConfigure(Options.DefaultName, options); } @@ -342,17 +406,36 @@ public PostConfigureOptions(string name, TDep1 dependency1, TDep2 dependency2, T /// public Action Action { get; } + /// + /// The first dependency. + /// public TDep1 Dependency1 { get; } + /// + /// The second dependency. + /// public TDep2 Dependency2 { get; } + /// + /// The third dependency. + /// public TDep3 Dependency3 { get; } + /// + /// The fourth dependency. + /// public TDep4 Dependency4 { get; } + /// + /// The fifth dependency. + /// public TDep5 Dependency5 { get; } - + /// + /// Invoked to configure a TOptions instance. + /// + /// The name of the options instance being configured. + /// The options instance to configured. public virtual void PostConfigure(string name, TOptions options) { if (options == null) @@ -367,6 +450,10 @@ public virtual void PostConfigure(string name, TOptions options) } } + /// + /// Invoked to configure a TOptions instance using the . + /// + /// The options instance to configured. public void PostConfigure(TOptions options) => PostConfigure(Options.DefaultName, options); } diff --git a/src/Microsoft.Extensions.Options/ValidateOptions.cs b/src/Microsoft.Extensions.Options/ValidateOptions.cs index 0d8e5b63d0c..94be1dd9982 100644 --- a/src/Microsoft.Extensions.Options/ValidateOptions.cs +++ b/src/Microsoft.Extensions.Options/ValidateOptions.cs @@ -11,6 +11,12 @@ namespace Microsoft.Extensions.Options /// The instance being validated. public class ValidateOptions : IValidateOptions where TOptions : class { + /// + /// Constructor. + /// + /// + /// + /// public ValidateOptions(string name, Func validation, string failureMessage) { Name = name; @@ -33,6 +39,12 @@ public ValidateOptions(string name, Func validation, string fail /// public string FailureMessage { get; } + /// + /// Validates a specific named options instance (or all when name is null). + /// + /// The name of the options instance being validated. + /// The options instance. + /// The result. public ValidateOptionsResult Validate(string name, TOptions options) { // Null name is used to configure all named options. diff --git a/test/Microsoft.Extensions.Options.Test/Microsoft.Extensions.Options.Test.csproj b/test/Microsoft.Extensions.Options.Test/Microsoft.Extensions.Options.Test.csproj index 2b82dd3f572..7c350060cdf 100644 --- a/test/Microsoft.Extensions.Options.Test/Microsoft.Extensions.Options.Test.csproj +++ b/test/Microsoft.Extensions.Options.Test/Microsoft.Extensions.Options.Test.csproj @@ -6,6 +6,7 @@ + diff --git a/test/Microsoft.Extensions.Options.Test/OptionsBuilderTest.cs b/test/Microsoft.Extensions.Options.Test/OptionsBuilderTest.cs index 71b969895bc..23a15593a05 100644 --- a/test/Microsoft.Extensions.Options.Test/OptionsBuilderTest.cs +++ b/test/Microsoft.Extensions.Options.Test/OptionsBuilderTest.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Reflection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -393,7 +395,7 @@ private void ValidateFailure(OptionsValidationException e, string name { errors = new string[] { "A validation error has occured." }; } - Assert.True(errors.SequenceEqual(e.Failures)); + Assert.True(errors.SequenceEqual(e.Failures), "Expected: " + String.Join(" - ", e.Failures)); } [Fact] @@ -475,7 +477,8 @@ public void Validate() try { getMethod.Invoke(monitor, new object[] { namedInstance }); - } catch (Exception e) + } + catch (Exception e) { if (e.InnerException is OptionsValidationException) { @@ -513,5 +516,105 @@ public void CanValidateOptionsEagerly() ValidateFailure(error, Options.DefaultName, "A validation error has occured.", "Virtual", "Integer"); } + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class FromAttribute : ValidationAttribute + { + public string Accepted { get; set; } + + public override bool IsValid(object value) + => value == null || value.ToString() == Accepted; + } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class DepValidator : ValidationAttribute + { + public string Target { get; set; } + + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + object instance = validationContext.ObjectInstance; + Type type = instance.GetType(); + var dep1 = type.GetProperty("Dep1")?.GetValue(instance); + var dep2 = type.GetProperty(Target)?.GetValue(instance); + if (dep1 == dep2) + { + return ValidationResult.Success; + } + return new ValidationResult("Dep1 != "+Target, new string[] { "Dep1", Target }); + } + } + + private class AnnotatedOptions + { + [Required] + public string Required { get; set; } + + [StringLength(5, ErrorMessage = "Too long.")] + public string StringLength { get; set; } + + [Range(-5, 5, ErrorMessage = "Out of range.")] + public int IntRange { get; set; } + + [From(Accepted = "USA")] + public string Custom { get; set; } + + [DepValidator(Target = "Dep2")] + public string Dep1 { get; set; } + public string Dep2 { get; set; } + } + + [Fact] + public void CanValidateDataAnnotations() + { + var services = new ServiceCollection(); + services.AddOptions() + .Configure(o => + { + o.StringLength = "111111"; + o.IntRange = 10; + o.Custom = "nowhere"; + o.Dep1 = "Not dep2"; + }) + .ValidateDataAnnotations(); + + var sp = services.BuildServiceProvider(); + + var error = Assert.Throws(() => sp.GetRequiredService>().Value); + ValidateFailure(error, Options.DefaultName, + @"DataAnnotation validation failed for members Required with the error 'The Required field is required.'. +DataAnnotation validation failed for members StringLength with the error 'Too long.'. +DataAnnotation validation failed for members IntRange with the error 'Out of range.'. +DataAnnotation validation failed for members Custom with the error 'The field Custom is invalid.'. +DataAnnotation validation failed for members Dep1, Dep2 with the error 'Dep1 != Dep2'."); + } + + [Fact] + public void CanValidateMixDataAnnotations() + { + var services = new ServiceCollection(); + services.AddOptions() + .Configure(o => + { + o.StringLength = "111111"; + o.IntRange = 10; + o.Custom = "nowhere"; + o.Dep1 = "Not dep2"; + }) + .ValidateDataAnnotations() + .Validate(o => o.Custom != "nowhere", "I don't want to go to nowhere!"); + + var sp = services.BuildServiceProvider(); + + var error = Assert.Throws(() => sp.GetRequiredService>().Value); + ValidateFailure(error, Options.DefaultName, + @"DataAnnotation validation failed for members Required with the error 'The Required field is required.'. +DataAnnotation validation failed for members StringLength with the error 'Too long.'. +DataAnnotation validation failed for members IntRange with the error 'Out of range.'. +DataAnnotation validation failed for members Custom with the error 'The field Custom is invalid.'. +DataAnnotation validation failed for members Dep1, Dep2 with the error 'Dep1 != Dep2'.", + "I don't want to go to nowhere!"); + } + + } -} \ No newline at end of file +} \ No newline at end of file