未验证 提交 c9996085 编写于 作者: S Steve Harter 提交者: GitHub

Add extension methods for creating OptionsBuilder with ValidateOnStart support (#89973)

上级 ad5ba123
......@@ -5,7 +5,6 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Xunit;
......@@ -124,6 +123,32 @@ public async Task ValidateOnStart_NamedOptions_ValidatesFailureOnStart()
}
}
[Fact]
public async Task ValidateOnStart_NamedOptions_ValidatesFailureOnStart_AddOptionsWithValidateOnStart()
{
var hostBuilder = CreateHostBuilder(services =>
{
services.AddOptions().AddSingleton(new FakeService());
services
.AddOptionsWithValidateOnStart<FakeSettings>("named")
.Configure<FakeService>((o, _) =>
{
o.Name = "named";
})
.Validate(o => o.Name == null, "trigger validation failure for named option!");
});
using (var host = hostBuilder.Build())
{
var error = await Assert.ThrowsAsync<OptionsValidationException>(async () =>
{
await host.StartAsync();
});
ValidateFailure<FakeSettings>(error, 1, "trigger validation failure for named option!");
}
}
[Fact]
private async Task ValidateOnStart_AddNamedOptionsMultipleTimesForSameType_BothGetTriggered()
{
......@@ -195,6 +220,61 @@ private async Task ValidateOnStart_AddEagerValidation_DoesValidationWhenHostStar
Assert.True(validateCalled);
}
[Fact]
private async Task ValidateOnStart_AddEagerValidation_DoesValidationWhenHostStartsWithNoFailure_AddOptionsWithValidateOnStart()
{
bool validateCalled = false;
var hostBuilder = CreateHostBuilder(services =>
{
// Adds eager validation using ValidateOnStart
services.AddOptionsWithValidateOnStart<ComplexOptions>("correct_configuration")
.Configure(o => o.Boolean = true)
.Validate(o =>
{
validateCalled = true;
return o.Boolean;
}, "correct_configuration");
});
using (var host = hostBuilder.Build())
{
await host.StartAsync();
}
Assert.True(validateCalled);
}
[Fact]
private async void CanValidateOptionsEagerly_AddOptionsWithValidateOnStart_IValidateOptions()
{
var hostBuilder = CreateHostBuilder(services =>
services.AddOptionsWithValidateOnStart<ComplexOptions, ComplexOptionsValidator>()
.Configure(o => o.Boolean = false));
using (var host = hostBuilder.Build())
{
var error = await Assert.ThrowsAsync<OptionsValidationException>(async () =>
{
await host.StartAsync();
});
ValidateFailure<ComplexOptions>(error, 1, "Boolean != true");
}
}
private class ComplexOptionsValidator : IValidateOptions<ComplexOptions>
{
public ValidateOptionsResult Validate(string name, ComplexOptions options)
{
if (options.Boolean == true)
{
return ValidateOptionsResult.Success;
}
return ValidateOptionsResult.Fail("Boolean != true");
}
}
[Fact]
private async Task ValidateOnStart_AddLazyValidation_SkipsValidationWhenHostStarts()
{
......
......@@ -13,6 +13,8 @@ public static partial class OptionsBuilderExtensions
public static partial class OptionsServiceCollectionExtensions
{
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { throw null; }
public static Microsoft.Extensions.Options.OptionsBuilder<TOptions> AddOptionsWithValidateOnStart<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] TOptions>(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, string? name = null) where TOptions : class { throw null; }
public static Microsoft.Extensions.Options.OptionsBuilder<TOptions> AddOptionsWithValidateOnStart<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] TOptions, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TValidateOptions>(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, string? name = null) where TOptions : class where TValidateOptions : class, Microsoft.Extensions.Options.IValidateOptions<TOptions> { throw null; }
public static Microsoft.Extensions.Options.OptionsBuilder<TOptions> AddOptions<TOptions>(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) where TOptions : class { throw null; }
public static Microsoft.Extensions.Options.OptionsBuilder<TOptions> AddOptions<TOptions>(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, string? name) where TOptions : class { throw null; }
public static Microsoft.Extensions.DependencyInjection.IServiceCollection ConfigureAll<TOptions>(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<TOptions> configureOptions) where TOptions : class { throw null; }
......@@ -141,7 +143,7 @@ public partial interface IPostConfigureOptions<in TOptions> where TOptions : cla
}
public partial interface IStartupValidator
{
public void Validate();
void Validate();
}
public partial interface IValidateOptions<TOptions> where TOptions : class
{
......@@ -323,7 +325,7 @@ public partial class ValidateOptionsResult
public static Microsoft.Extensions.Options.ValidateOptionsResult Fail(System.Collections.Generic.IEnumerable<string> failures) { throw null; }
public static Microsoft.Extensions.Options.ValidateOptionsResult Fail(string failureMessage) { throw null; }
}
public class ValidateOptionsResultBuilder
public partial class ValidateOptionsResultBuilder
{
public ValidateOptionsResultBuilder() { }
public void AddError(string error, string? propertyName = null) { }
......
......@@ -31,6 +31,47 @@ public static IServiceCollection AddOptions(this IServiceCollection services)
return services;
}
/// <summary>
/// Adds services required for using options and enforces options validation check on start rather than in runtime.
/// </summary>
/// <remarks>
/// The <seealso cref="OptionsBuilderExtensions.ValidateOnStart{TOptions}(OptionsBuilder{TOptions})"/> extension is called by this method.
/// </remarks>
/// <typeparam name="TOptions">The options type to be configured.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <param name="name">The name of the options instance.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static OptionsBuilder<TOptions> AddOptionsWithValidateOnStart<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(
this IServiceCollection services,
string? name = null)
where TOptions : class
{
return new OptionsBuilder<TOptions>(services, name ?? Options.Options.DefaultName).ValidateOnStart();
}
/// <summary>
/// Adds services required for using options and enforces options validation check on start rather than in runtime.
/// </summary>
/// <remarks>
/// The <seealso cref="OptionsBuilderExtensions.ValidateOnStart{TOptions}(OptionsBuilder{TOptions})"/> extension is called by this method.
/// </remarks>
/// <typeparam name="TOptions">The options type to be configured.</typeparam>
/// <typeparam name="TValidateOptions">The <see cref="IValidateOptions{TOptions}"/> validator type.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <param name="name">The name of the options instance.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static OptionsBuilder<TOptions> AddOptionsWithValidateOnStart<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TValidateOptions>(
this IServiceCollection services,
string? name = null)
where TOptions : class
where TValidateOptions : class, IValidateOptions<TOptions>
{
services.AddOptions().TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions<TOptions>, TValidateOptions>());
return new OptionsBuilder<TOptions>(services, name ?? Options.Options.DefaultName).ValidateOnStart();
}
/// <summary>
/// Registers an action used to configure a particular type of options.
/// Note: These are run before all <seealso cref="PostConfigure{TOptions}(IServiceCollection, Action{TOptions})"/>.
......
......@@ -5,9 +5,9 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Xunit;
namespace Microsoft.Extensions.Options.Tests
......@@ -321,6 +321,18 @@ public void BadValidatorFailsGracefully()
var error = Assert.Throws<NotImplementedException>(() => sp.GetRequiredService<IOptions<FakeOptions>>().Value);
}
private class ComplexOptionsValidator : IValidateOptions<ComplexOptions>
{
public ValidateOptionsResult Validate(string name, ComplexOptions options)
{
if (options.Boolean == true)
{
return ValidateOptionsResult.Success;
}
return ValidateOptionsResult.Fail("Boolean != true");
}
}
private class MultiOptionValidator : IValidateOptions<ComplexOptions>, IValidateOptions<FakeOptions>
{
private readonly string _allowed;
......@@ -567,6 +579,34 @@ public void CanValidateOptionsEagerly()
ValidateFailure<ComplexOptions>(error, Options.DefaultName, 3, "A validation error has occurred.", "Virtual", "Integer");
}
[Fact]
public void CanValidateOptionsEagerly_AddOptionsWithValidateOnStart()
{
var services = new ServiceCollection();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions<ComplexOptions>, ComplexOptionsValidator>());
services
.AddOptionsWithValidateOnStart<ComplexOptions>()
.Configure(o => o.Boolean = false);
var sp = services.BuildServiceProvider();
// This doesn't really verify eager validation since we have no host to start.
var error = Assert.Throws<OptionsValidationException>(() => sp.GetRequiredService<IOptions<ComplexOptions>>().Value);
ValidateFailure<ComplexOptions>(error, Options.DefaultName, 1, "Boolean != true");
}
[Fact]
public void CanValidateOptionsEagerly_AddOptionsWithValidateOnStart_IValidateOptions()
{
var services = new ServiceCollection();
services.AddOptionsWithValidateOnStart<ComplexOptions, ComplexOptionsValidator>()
.Configure(o => o.Boolean = false);
var sp = services.BuildServiceProvider();
// This doesn't really verify eager validation since we have no host to start.
var error = Assert.Throws<OptionsValidationException>(() => sp.GetRequiredService<IOptions<ComplexOptions>>().Value);
ValidateFailure<ComplexOptions>(error, Options.DefaultName, 1, "Boolean != true");
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromAttribute : ValidationAttribute
{
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册