diff --git a/src/Microsoft.Extensions.Options/ConfigureNamedOptions.cs b/src/Microsoft.Extensions.Options/ConfigureNamedOptions.cs new file mode 100644 index 0000000..63d9f2d --- /dev/null +++ b/src/Microsoft.Extensions.Options/ConfigureNamedOptions.cs @@ -0,0 +1,54 @@ +// 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 +{ + /// + /// Implementation of IConfigureNamedOptions. + /// + /// + public class ConfigureNamedOptions : IConfigureNamedOptions where TOptions : class + { + /// + /// Constructor. + /// + /// The name of the options. + /// The action to register. + public ConfigureNamedOptions(string name, Action action) + { + Name = name; + Action = action; + } + + /// + /// The options name. + /// + public string Name { get; } + + /// + /// The configuration action. + /// + public Action Action { get; } + + /// + /// Invokes the registered configure Action if the name matches. + /// + /// + /// + public virtual void Configure(string name, TOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + // Null name is used to configure all named options. + if (Name == null || name == Name) + { + Action?.Invoke(options); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options/IConfigureNamedOptions.cs b/src/Microsoft.Extensions.Options/IConfigureNamedOptions.cs new file mode 100644 index 0000000..1ec9e1a --- /dev/null +++ b/src/Microsoft.Extensions.Options/IConfigureNamedOptions.cs @@ -0,0 +1,20 @@ +// 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. + +namespace Microsoft.Extensions.Options +{ + + /// + /// Represents something that configures the TOptions type. + /// + /// + public interface IConfigureNamedOptions where TOptions : class + { + /// + /// Invoked to configure a TOptions instance. + /// + /// The name of the options instance being configured. + /// The options instance to configure. + void Configure(string name, TOptions options); + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options/IOptionsCache.cs b/src/Microsoft.Extensions.Options/IOptionsCache.cs new file mode 100644 index 0000000..263a11f --- /dev/null +++ b/src/Microsoft.Extensions.Options/IOptionsCache.cs @@ -0,0 +1,22 @@ +// 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 +{ + /// + /// Used to cache TOptions instances. + /// + /// The type of options being requested. + public interface IOptionsCache where TOptions : class + { + TOptions GetOrAdd(string name, Func createOptions); + + bool TryAdd(string name, TOptions options); + + bool TryRemove(string name); + + // Do we need a Clear all? + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options/IOptionsFactory.cs b/src/Microsoft.Extensions.Options/IOptionsFactory.cs new file mode 100644 index 0000000..f4025bd --- /dev/null +++ b/src/Microsoft.Extensions.Options/IOptionsFactory.cs @@ -0,0 +1,17 @@ +// 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. + +namespace Microsoft.Extensions.Options +{ + /// + /// Used to create TOptions instances. + /// + /// The type of options being requested. + public interface IOptionsFactory where TOptions : class, new() + { + /// + /// Returns a configured TOptions instance with the given name. + /// + TOptions Create(string name); + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options/IOptionsService.cs b/src/Microsoft.Extensions.Options/IOptionsService.cs new file mode 100644 index 0000000..f81c5d5 --- /dev/null +++ b/src/Microsoft.Extensions.Options/IOptionsService.cs @@ -0,0 +1,21 @@ +// 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. + +namespace Microsoft.Extensions.Options +{ + /// + /// Used to retreive configured and validated TOptions instances. + /// + /// The type of options being requested. + public interface IOptionsService where TOptions : class, new() + { + /// + /// Returns a configured and validated TOptions instance with the given name. + /// + TOptions Get(string name); + + void Add(string name, TOptions options); + + bool Remove(string name); + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options/IOptionsValidator.cs b/src/Microsoft.Extensions.Options/IOptionsValidator.cs new file mode 100644 index 0000000..36d4101 --- /dev/null +++ b/src/Microsoft.Extensions.Options/IOptionsValidator.cs @@ -0,0 +1,17 @@ +// 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. + +namespace Microsoft.Extensions.Options +{ + /// + /// Used to validate TOptions instances. + /// + /// The type of options being requested. + public interface IOptionsValidator where TOptions : class, new() + { + /// + /// Validates the options instance. + /// + void Validate(string name, TOptions options); + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options/IValidateNamedOptions.cs b/src/Microsoft.Extensions.Options/IValidateNamedOptions.cs new file mode 100644 index 0000000..b11ccea --- /dev/null +++ b/src/Microsoft.Extensions.Options/IValidateNamedOptions.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. + + +namespace Microsoft.Extensions.Options +{ + /// + /// Represents something that validate the TOptions type. + /// + /// The type of options being requested. + public interface IValidateNamedOptions where TOptions : class + { + /// + /// The name of the options instance to validate. + /// + string Name { get; } + + /// + /// Invoked to validate a TOptions instance. + /// + /// The name of the options instance being validated. + /// The options instance to validate. + void Validate(string name, TOptions options); + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options/LegacyOptionsCache.cs b/src/Microsoft.Extensions.Options/LegacyOptionsCache.cs new file mode 100644 index 0000000..ec8b619 --- /dev/null +++ b/src/Microsoft.Extensions.Options/LegacyOptionsCache.cs @@ -0,0 +1,49 @@ +// 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.Threading; + +namespace Microsoft.Extensions.Options +{ + internal class LegacyOptionsCache where TOptions : class, new() + { + private readonly Func _createCache; + private object _cacheLock = new object(); + private bool _cacheInitialized; + private TOptions _options; + private IEnumerable> _setups; + + public LegacyOptionsCache(IEnumerable> setups) + { + _setups = setups; + _createCache = CreateOptions; + } + + private TOptions CreateOptions() + { + var result = new TOptions(); + if (_setups != null) + { + foreach (var setup in _setups) + { + setup.Configure(result); + } + } + return result; + } + + public virtual TOptions Value + { + get + { + return LazyInitializer.EnsureInitialized( + ref _options, + ref _cacheInitialized, + ref _cacheLock, + _createCache); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options/Microsoft.Extensions.Options.csproj b/src/Microsoft.Extensions.Options/Microsoft.Extensions.Options.csproj index f8b6248..e8c8fb3 100644 --- a/src/Microsoft.Extensions.Options/Microsoft.Extensions.Options.csproj +++ b/src/Microsoft.Extensions.Options/Microsoft.Extensions.Options.csproj @@ -4,7 +4,7 @@ Provides a strongly typed way of specifying and accessing settings using dependency injection. - netstandard1.0 + netstandard1.1 $(NoWarn);CS1591 true aspnetcore;options @@ -13,7 +13,6 @@ - - + diff --git a/src/Microsoft.Extensions.Options/OptionsCache.cs b/src/Microsoft.Extensions.Options/OptionsCache.cs index 2572ea9..e4f5dba 100644 --- a/src/Microsoft.Extensions.Options/OptionsCache.cs +++ b/src/Microsoft.Extensions.Options/OptionsCache.cs @@ -1,49 +1,65 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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.Threading; +using System.Collections.Concurrent; namespace Microsoft.Extensions.Options { - internal class OptionsCache where TOptions : class, new() + /// + /// Used to cache TOptions instances. + /// + /// The type of options being requested. + public class OptionsCache : IOptionsCache where TOptions : class { - private readonly Func _createCache; - private object _cacheLock = new object(); - private bool _cacheInitialized; - private TOptions _options; - private IEnumerable> _setups; + private readonly ConcurrentDictionary> _cache = new ConcurrentDictionary>(StringComparer.Ordinal); - public OptionsCache(IEnumerable> setups) + public virtual TOptions GetOrAdd(string name, Func createOptions) { - _setups = setups; - _createCache = CreateOptions; + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + if (createOptions == null) + { + throw new ArgumentNullException(nameof(createOptions)); + } + return _cache.GetOrAdd(name, new Lazy(createOptions)).Value; } - private TOptions CreateOptions() + /// + /// Tries to adds a new option to the cache, will return false if the name already exists. + /// + /// The name of the options instance. + /// The options instance. + /// Whether anything was added. + public virtual bool TryAdd(string name, TOptions options) { - var result = new TOptions(); - if (_setups != null) + if (name == null) { - foreach (var setup in _setups) - { - setup.Configure(result); - } + throw new ArgumentNullException(nameof(name)); } - return result; + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + return _cache.TryAdd(name, new Lazy(() => options)); } - public virtual TOptions Value + /// + /// Try to remove an options instance. + /// + /// The name of the options instance. + /// Whether anything was removed. + public virtual bool TryRemove(string name) { - get + if (name == null) { - return LazyInitializer.EnsureInitialized( - ref _options, - ref _cacheInitialized, - ref _cacheLock, - _createCache); + throw new ArgumentNullException(nameof(name)); } + return _cache.TryRemove(name, out var ignored); } + + // Do we need a Clear all? } } \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options/OptionsFactory.cs b/src/Microsoft.Extensions.Options/OptionsFactory.cs new file mode 100644 index 0000000..70b49ee --- /dev/null +++ b/src/Microsoft.Extensions.Options/OptionsFactory.cs @@ -0,0 +1,35 @@ +// 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.Collections.Generic; + +namespace Microsoft.Extensions.Options +{ + /// + /// Implementation of IOptionsFactory. + /// + /// The type of options being requested. + public class OptionsFactory : IOptionsFactory where TOptions : class, new() + { + private readonly IEnumerable> _setups; + + /// + /// Initializes a new instance with the specified options configurations. + /// + /// The configuration actions to run. + public OptionsFactory(IEnumerable> setups) + { + _setups = setups; + } + + public TOptions Create(string name) + { + var options = new TOptions(); + foreach (var setup in _setups) + { + setup.Configure(name, options); + } + return options; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options/OptionsManager.cs b/src/Microsoft.Extensions.Options/OptionsManager.cs index a0ee9fb..d85bf97 100644 --- a/src/Microsoft.Extensions.Options/OptionsManager.cs +++ b/src/Microsoft.Extensions.Options/OptionsManager.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.Options /// public class OptionsManager : IOptions, IOptionsSnapshot where TOptions : class, new() { - private OptionsCache _optionsCache; + private LegacyOptionsCache _optionsCache; /// /// Initializes a new instance with the specified options configurations. @@ -19,7 +19,7 @@ namespace Microsoft.Extensions.Options /// The configuration actions to run. public OptionsManager(IEnumerable> setups) { - _optionsCache = new OptionsCache(setups); + _optionsCache = new LegacyOptionsCache(setups); } /// diff --git a/src/Microsoft.Extensions.Options/OptionsMonitor.cs b/src/Microsoft.Extensions.Options/OptionsMonitor.cs index 9f00328..a4e07f9 100644 --- a/src/Microsoft.Extensions.Options/OptionsMonitor.cs +++ b/src/Microsoft.Extensions.Options/OptionsMonitor.cs @@ -13,7 +13,7 @@ namespace Microsoft.Extensions.Options /// public class OptionsMonitor : IOptionsMonitor where TOptions : class, new() { - private OptionsCache _optionsCache; + private LegacyOptionsCache _optionsCache; private readonly IEnumerable> _setups; private readonly IEnumerable> _sources; private List> _listeners = new List>(); @@ -27,7 +27,7 @@ public OptionsMonitor(IEnumerable> setups, IEnumerab { _sources = sources; _setups = setups; - _optionsCache = new OptionsCache(setups); + _optionsCache = new LegacyOptionsCache(setups); foreach (var source in _sources) { @@ -39,7 +39,7 @@ public OptionsMonitor(IEnumerable> setups, IEnumerab private void InvokeChanged() { - _optionsCache = new OptionsCache(_setups); + _optionsCache = new LegacyOptionsCache(_setups); foreach (var listener in _listeners) { listener?.Invoke(_optionsCache.Value); diff --git a/src/Microsoft.Extensions.Options/OptionsService.cs b/src/Microsoft.Extensions.Options/OptionsService.cs new file mode 100644 index 0000000..7eac496 --- /dev/null +++ b/src/Microsoft.Extensions.Options/OptionsService.cs @@ -0,0 +1,72 @@ +// 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.Collections.Generic; +using System.Collections.Concurrent; +using System; + +namespace Microsoft.Extensions.Options +{ + /// + /// Implementation of IOptionsFactory. + /// + /// The type of options being requested. + public class OptionsService : IOptionsService where TOptions : class, new() + { + private readonly IOptionsCache _cache; + private readonly IOptionsFactory _factory; + private readonly IOptionsValidator _validator; + + /// + /// Initializes a new instance with the specified options configurations. + /// + /// The cache to use. + /// The factory to use to create options. + /// The validator used to validate options. + public OptionsService(IOptionsCache cache, IOptionsFactory factory, IOptionsValidator validator) + { + _cache = cache; + _factory = factory; + _validator = validator; + } + + public virtual void Add(string name, TOptions options) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + if (!_cache.TryAdd(name, options)) + { + throw new InvalidOperationException("An option named {name} already exists."); + } + } + + public virtual TOptions Get(string name) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + return _cache.GetOrAdd(name, () => + { + var options = _factory.Create(name); + _validator.Validate(name, options); + return options; + }); + } + + public virtual bool Remove(string name) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + return _cache.TryRemove(name); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options/OptionsServiceCollectionExtensions.cs b/src/Microsoft.Extensions.Options/OptionsServiceCollectionExtensions.cs index aa7343a..b447e98 100644 --- a/src/Microsoft.Extensions.Options/OptionsServiceCollectionExtensions.cs +++ b/src/Microsoft.Extensions.Options/OptionsServiceCollectionExtensions.cs @@ -27,6 +27,10 @@ public static IServiceCollection AddOptions(this IServiceCollection services) services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>))); services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>))); services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>))); + services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsService<>), typeof(OptionsService<>))); + services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>))); + services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsValidator<>), typeof(OptionsValidator<>))); + services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsCache<>), typeof(OptionsCache<>))); return services; } @@ -53,5 +57,101 @@ public static IServiceCollection Configure(this IServiceCollection ser services.AddSingleton>(new ConfigureOptions(configureOptions)); return services; } + + /// + /// Registers an action used to configure a particular type of options. + /// + /// The options type to be configured. + /// The to add the services to. + /// The name of the options instance. + /// The action used to configure the options. + /// The so that additional calls can be chained. + public static IServiceCollection Configure(this IServiceCollection services, string name, Action configureOptions) + where TOptions : class + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + services.AddSingleton>(new ConfigureNamedOptions(name, configureOptions)); + return services; + } + + public static IServiceCollection ConfigureAll(this IServiceCollection services, Action configureOptions) + where TOptions : class + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + // REVIEW: should this ignore the non named options? ConfigureAllNamed? + services.Configure(configureOptions); + services.AddSingleton>(new ConfigureNamedOptions(name: null, action: configureOptions)); + return services; + } + + /// + /// Registers an action used to validate options with a specific name. + /// + /// The options type to be configured. + /// The to add the services to. + /// The name of the options instance. + /// The action used to validate the options. + /// The so that additional calls can be chained. + public static IServiceCollection Validate(this IServiceCollection services, string name, Action validateOptions) + where TOptions : class + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (validateOptions == null) + { + throw new ArgumentNullException(nameof(validateOptions)); + } + + services.AddSingleton>(new ValidateNamedOptions(name, validateOptions)); + return services; + } + + public static IServiceCollection ValidateAll(this IServiceCollection services, Action validateOptions) + where TOptions : class + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (validateOptions == null) + { + throw new ArgumentNullException(nameof(validateOptions)); + } + + services.AddSingleton>(new ValidateNamedOptions(name: null, action: validateOptions)); + return services; + } } } \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options/OptionsValidator.cs b/src/Microsoft.Extensions.Options/OptionsValidator.cs new file mode 100644 index 0000000..8c53757 --- /dev/null +++ b/src/Microsoft.Extensions.Options/OptionsValidator.cs @@ -0,0 +1,33 @@ +// 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.Collections.Generic; + +namespace Microsoft.Extensions.Options +{ + /// + /// Implementation of IOptionsValidator. + /// + /// The type of options being requested. + public class OptionsValidator : IOptionsValidator where TOptions : class, new() + { + private readonly IEnumerable> _checks; + + /// + /// Initializes a new instance with the specified options configurations. + /// + /// The validation actions to run. + public OptionsValidator(IEnumerable> validations) + { + _checks = validations; + } + + public void Validate(string name, TOptions options) + { + foreach (var check in _checks) + { + check.Validate(name, options); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Options/ValidateNamedOptions.cs b/src/Microsoft.Extensions.Options/ValidateNamedOptions.cs new file mode 100644 index 0000000..5914ab8 --- /dev/null +++ b/src/Microsoft.Extensions.Options/ValidateNamedOptions.cs @@ -0,0 +1,57 @@ +// 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 +{ + + /// + /// Implementation of IValidateNamedOptions. + /// + /// The type of options being requested. + public class ValidateNamedOptions : IValidateNamedOptions where TOptions : class + { + public ValidateNamedOptions() { } + + /// + /// Constructor. + /// + /// The name of the options. + /// The action to register. + public ValidateNamedOptions(string name, Action action) + { + Name = name; + Action = action; + } + + /// + /// The options name. + /// + public string Name { get; set; } + + /// + /// The configuration action. + /// + public Action Action { get; set; } + + /// + /// Invokes the registered validate Action if the name matches. + /// + /// + /// + public virtual void Validate(string name, TOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + // Null name is used to configure all named options. + if (Name == null || name == Name) + { + Action?.Invoke(options); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Extensions.Options.Test/OptionsServiceTests.cs b/test/Microsoft.Extensions.Options.Test/OptionsServiceTests.cs new file mode 100644 index 0000000..a45df76 --- /dev/null +++ b/test/Microsoft.Extensions.Options.Test/OptionsServiceTests.cs @@ -0,0 +1,148 @@ +// 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 Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Options.Tests +{ + public class OptionsFactoryTest + { + [Fact] + public void CanResolveNamedOptions() + { + var services = new ServiceCollection().AddOptions(); + + services.Configure("1", options => + { + options.Message = "one"; + }); + services.Configure("2", options => + { + options.Message = "two"; + }); + + var sp = services.BuildServiceProvider(); + var option = sp.GetRequiredService>(); + Assert.Equal("one", option.Get("1").Message); + Assert.Equal("two", option.Get("2").Message); + } + + [Fact] + public void FactoryValidatesOptions() + { + var services = new ServiceCollection().AddOptions(); + services.Validate("1", options => + { + if (string.IsNullOrEmpty(options.Message)) + { + throw new ArgumentNullException(); + } + }); + + var sp = services.BuildServiceProvider(); + var option = sp.GetRequiredService>(); + Assert.Throws(() => option.Get("1")); + } + + [Fact] + public void FactoryCanConfigureAllOptions() + { + var services = new ServiceCollection().AddOptions(); + services.ConfigureAll(o => o.Message = "Default"); + + var sp = services.BuildServiceProvider(); + var option = sp.GetRequiredService>(); + Assert.Equal("Default", option.Get("1").Message); + Assert.Equal("Default", option.Get("2").Message); + } + + [Fact] + public void FactoryCanValidateAllOptions() + { + var services = new ServiceCollection().AddOptions(); + services.ValidateAll(options => + { + if (string.IsNullOrEmpty(options.Message)) + { + throw new ArgumentNullException(); + } + }); + + var sp = services.BuildServiceProvider(); + var option = sp.GetRequiredService>(); + Assert.Throws(() => option.Get("1")); + Assert.Throws(() => option.Get("2")); + } + + [Fact] + public void FactoryConfigureAndValidateAllPlayWellTogether() + { + var services = new ServiceCollection().AddOptions(); + services.ConfigureAll(o => o.Message = "Default"); + services.Configure("NotDefault", o => o.Message = "NotDefault"); + services.Configure("Throws", o => o.Message = null); + services.Validate("NotDefault", options => + { + if (options.Message == "Default") + { + throw new Exception(); + } + }); + + services.ValidateAll(options => + { + if (string.IsNullOrEmpty(options.Message)) + { + throw new ArgumentNullException(); + } + }); + + var sp = services.BuildServiceProvider(); + var option = sp.GetRequiredService>(); + Assert.Equal("NotDefault", option.Get("NotDefault").Message); + Assert.Equal("Default", option.Get("Default").Message); + Assert.Throws(() => option.Get("Throws")); + } + + [Fact] + public void FactoryConfiguresInRegistrationOrder() + { + var services = new ServiceCollection().AddOptions(); + services.Configure("-", o => o.Message += "-"); + services.ConfigureAll(o => o.Message += "A"); + services.Configure("+", o => o.Message += "+"); + services.ConfigureAll(o => o.Message += "B"); + services.ConfigureAll(o => o.Message += "C"); + services.Configure("+", o => o.Message += "+"); + services.Configure("-", o => o.Message += "-"); + + var sp = services.BuildServiceProvider(); + var option = sp.GetRequiredService>(); + Assert.Equal("ABC", option.Get("1").Message); + Assert.Equal("A+BC+", option.Get("+").Message); + Assert.Equal("-ABC-", option.Get("-").Message); + } + + [Fact] + public void FactoryValidatesInRegistrationOrder() + { + var services = new ServiceCollection().AddOptions(); + services.Validate("-", o => o.Message += "-"); + services.ValidateAll(o => o.Message += "A"); + services.Validate("+", o => o.Message += "+"); + services.ValidateAll(o => o.Message += "B"); + services.ValidateAll(o => o.Message += "C"); + services.Validate("+", o => o.Message += "+"); + services.Validate("-", o => o.Message += "-"); + + var sp = services.BuildServiceProvider(); + var option = sp.GetRequiredService>(); + Assert.Equal("ABC", option.Get("1").Message); + Assert.Equal("A+BC+", option.Get("+").Message); + Assert.Equal("-ABC-", option.Get("-").Message); + } + + } +} \ No newline at end of file