-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Developers can get immediate feedback on validation problems #36391
Comments
I think @andrewlock did something similar for Configuration. Andrew could you please share some of your thoughts here |
I wrote a post about my approach here: https://andrewlock.net/adding-validation-to-strongly-typed-configuration-objects-in-asp-net-core/, and I have NuGet package to provide the funcitonality: https://github.com/andrewlock/NetEscapades.Configuration/blob/master/src/NetEscapades.Configuration.Validation Essentially, I implement a simple interface in my options objects ( Then I have a public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
foreach (var validatableObject in _validatableObjects)
{
validatableObject.Validate();
}
//don't alter the configuration
return next;
} Any exceptions will happen on app startup. There's a few caveats to this approach:
|
Thanks @andrewlock I'm guessing we will end up with something similar just in a more generic host friendly way, that probably uses options itself to configure what option instances to validate at startup. |
Out of time to get this in this week for preview2, will start a PR this week targeting preview3 |
Discussed with @ajcvickers current thinking will be to add the add a way to register startup actions which happen to do options validation. public class StartupOptions {
public IEnumerable<Action> StartupActions { get; set; } = new List<Action>();
}
// The Validation overloads that request eager validation would register an action something like this
services.Configure<StartupOptions, IOptionsMonitor<TOptions>>(
(o,mon) => o.StartupActions.Add(() => mon.Get(optionsName))
// Hosts would just need to invoke the startup actions Thoughts @davidfowl ? |
Per review with @DamianEdwards @davidfowl @ajcvickers @Eilon we are going to ship 2.2 without eager validation The existing eager validation code in the PR will be moved into the tests and referenced/explained in the 2.2 samples/documentation so we can gather feedback before adding official support |
I have created a small class (based on the idea by @andrewlock of using an Usage: public void ConfigureServices(IServiceCollection services)
{
services.ConfigureWithDataAnnotationsValidation<MyOptions>(
Configuration.GetSection("MyOptionsPath"),
validateAtStartup: true
);
} Class: /// <summary>
/// Extension methods for adding configuration related options services to the DI container.
/// </summary>
public static class OptionsConfigurationServiceCollectionExtensions
{
/// <summary>
/// Registers a configuration instance which <typeparamref name="TOptions"/> will bind against and validates the instance
/// on by using <see cref="DataAnnotationValidateOptions{TOptions}"/>.
/// </summary>
/// <typeparam name="TOptions">The type of options being configured.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <param name="config">The configuration being bound.</param>
/// <param name="validateAtStartup">Indicates if the options should be validated during application startup.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection ConfigureWithDataAnnotationsValidation<TOptions>(this IServiceCollection services, IConfiguration config, bool validateAtStartup = false) where TOptions : class
{
services.Configure<TOptions>(config);
services.AddSingleton<IValidateOptions<TOptions>>(new DataAnnotationValidateOptions<TOptions>(Microsoft.Extensions.Options.Options.DefaultName));
if (validateAtStartup)
{
ValidateAtStartup(services, typeof(TOptions));
}
return services;
}
/// <summary>
/// Registers a type of options to be validated during application startup.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <param name="type">The type of options to validate.</param>
static void ValidateAtStartup(IServiceCollection services, Type type)
{
var existingService = services.Select(x => x.ImplementationInstance).OfType<StartupOptionsValidation>().FirstOrDefault();
if (existingService == null)
{
existingService = new StartupOptionsValidation();
services.AddSingleton<IStartupFilter>(existingService);
}
existingService.OptionsTypes.Add(type);
}
/// <summary>
/// A startup filter that validates option instances during application startup.
/// </summary>
class StartupOptionsValidation : IStartupFilter
{
IList<Type> _optionsTypes;
/// <summary>
/// The type of options to validate.
/// </summary>
public IList<Type> OptionsTypes => _optionsTypes ?? (_optionsTypes = new List<Type>());
/// <inheritdoc/>
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return builder =>
{
if (_optionsTypes != null)
{
foreach (var optionsType in _optionsTypes)
{
var options = builder.ApplicationServices.GetService(typeof(IOptions<>).MakeGenericType(optionsType));
if (options != null)
{
// Retrieve the value to trigger validation
var optionsValue = ((IOptions<object>)options).Value;
}
}
}
next(builder);
};
}
}
} |
The empty error message of A possible implementation: /// <summary>
/// Thrown when options validation fails.
/// </summary>
public class OptionsValidationException : Exception
{
string _message;
/// <inheritdoc/>
public override string Message => _message ?? (_message = CreateExceptionMessage());
/// <summary>
/// Constructor.
/// </summary>
/// <param name="optionsName">The name of the options instance that failed.</param>
/// <param name="optionsType">The options type that failed.</param>
/// <param name="failureMessages">The validation failure messages.</param>
public OptionsValidationException(string optionsName, Type optionsType, IReadOnlyCollection<string> failureMessages)
{
Failures = failureMessages ?? Array.Empty<string>();
OptionsType = optionsType ?? throw new ArgumentNullException(nameof(optionsType));
OptionsName = optionsName ?? throw new ArgumentNullException(nameof(optionsName));
}
/// <summary>
/// The name of the options instance that failed.
/// </summary>
public string OptionsName { get; }
/// <summary>
/// The type of the options that failed.
/// </summary>
public Type OptionsType { get; }
/// <summary>
/// The validation failures.
/// </summary>
public IReadOnlyCollection<string> Failures { get; }
/// <summary>
/// Returns an error message.
/// </summary>
/// <returns></returns>
string CreateExceptionMessage()
{
if (Failures.Count > 0)
{
return $"{Failures.Count} validation error(s) occurred while validating options of type '{OptionsType.FullName}'. The first error is: {Failures.First()}";
}
else
{
return $"One or more validation errors occurred while validating options of type '{OptionsType.FullName}'.";
}
}
} |
No plans to add eager validation yet |
Thank you for all the great ideas in this thread. I wanted to retain the Validate and ValidateDataAnnotations provided by OptionsBuilder but enable eager validation, the patterns above led me to: Usage: services.AddOptions<LoggingSettings>()
.ValidateDataAnnotations()
.ValidateEagerly(); Classes: public class StartupOptionsValidation<T> : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return builder =>
{
var options = builder.ApplicationServices.GetService(typeof(IOptions<>).MakeGenericType(typeof(T)));
if (options != null)
{
// Retrieve the value to trigger validation
var optionsValue = ((IOptions<object>)options).Value;
}
next(builder);
};
}
} public static class OptionsBuilderValidationExtensions
{
public static OptionsBuilder<TOptions> ValidateEagerly<TOptions>(this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class
{
optionsBuilder.Services.AddTransient<IStartupFilter, StartupOptionsValidation<TOptions>>();
return optionsBuilder;
}
} |
Just be aware that using Another thing to consider: how should be "reloadOnChange" handled? Last thing I want is to throw at runtime. With current instrumentation of |
For container loads, that genericHost is perfect for, changing of config at runtime would not be a common usecase. |
@ArgTang that is true. But there are people who are using generic host without containers (like I am at work) and it might be a common use case to change config at runtime for these people. |
I couldn't figure out the best area label to add to this issue. Please help me learn by adding exactly one area label. |
For a background worker (console and/or service), I am already using Adapting the above, I did the following: public class Program
{
public static int Main(string[] args)
{
try
{
CreateHostBuilder(args).Build()
.ValidateOptions<LoggingOptions>() // new line per option to validate.
.ValidateOptions<CredentialsOptions>()
.Run();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
return 1;
}
}
// etc.
} public static class OptionsBuilderValidationExtensions
{
public static IHost ValidateOptions<T>(this IHost host)
{
object options = host.Services.GetService(typeof(IOptions<>).MakeGenericType(typeof(T)));
if (options != null)
{
// Retrieve the value to trigger validation
var optionsValue = ((IOptions<object>)options).Value;
}
return host;
}
} |
Has anyone written any tests for these solutions? @michaelgregson I've implemented somehting very similar to your solution... |
Added api-suggesion label to this issue. The best next step on this would be to prepare the API proposal, usage, etc. in the issue page here (following https://github.com/dotnet/runtime/blob/master/docs/project/api-review-process.md). |
I'm not a fan of this proposal (I don't think there should be an extension method on the host). Also that implementation is more complex than it needs to be. |
@maryamariyan can you please ensure User stories have a title in the form "XXX can YY" indicating what user experience changes. Take a look at other titles: https://themesof.net/?q=is:open%20kinds:ub%20team:Libraries Same for #44517. |
namespace Microsoft.Extensions.DependencyInjection
{
public static class OptionsBuilderDataAnnotationsExtensions
{
// Already exists:
// public static OptionsBuilder<TOptions> ValidateDataAnnotations<TOptions>(this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class;
public static OptionsBuilder<TOptions> ValidateOnStartup<TOptions>(this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class;
}
} |
I prefer ValidateOnStartup, though people may think it has something to do with the Startup class. What about |
The ValidateDataAnnotationsOnStartup would validate data annotations, services.AddOptions<MyOptions>()
.Configure(o => o.Boolean = false)
.ValidateDataAnnotationsOnStartup(); // or ValidateDataAnnotationsOnStart() would be same as calling (shorthand for): services.AddOptions<MyOptions>()
.Configure(o => o.Boolean = false)
.ValidateDataAnnotations()
.ValidateOnStartup(); // or ValidateOnStart(); but it is also possible to validate options that are specified using a delegate without data annotations, e.g. services.AddOptions<MyOptions>()
.Configure(o => o.Boolean = false)
.Validate(o => o.Boolean)
.ValidateOnStartup(); // or ValidateOnStart(); Doesn't seem like we'd need the shorthand for DataAnnotations, so the plan is to just add |
I like ValidateOnStart too, eagerly isn't clear exactly what that means |
namespace Microsoft.Extensions.DependencyInjection
{
public static class OptionsBuilderValidationExtensions
{
public static OptionsBuilder<TOptions> ValidateOnStart<TOptions>(this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class;
}
} |
👍🏾 |
Updated by @maryamariyan:
Goal
When an application starts, we want to get immediate feedback on validation problems. e.g. we would like to get exceptions thrown on app startup rather than later.
Benefits of eager validation:
IOptions<T>
is requestedAPI Proposal
ValidateOnStart
feature is implemented as extension toOptionsBuilder<TOptions>
To allow for eager validation, an API suggestion is to add an extension method on OptionsBuilder, (not IHost).
Usage:
APIs:
According to usage, we'd need the APIs below:
Focus of this issue:
The focus here is on eager validation at application startup, and these APIs don't trigger for IOptionsSnapshot and IOptionsMonitor, where values may get recomputed on every request or upon configuration reload after the startup has completed.
IOptions<TOptions>
:IOptionsSnapshot<TOptions>
:May be recomputed on every request, and supports named options
IOptionsMonitor<TOptions>
:Is registered as a singleton, supports named options, change notifications, configuration reloads, and can be injected to any service lifetime.
Original Description (click to view)
AB#1244419
From exp review with @ajcvickers @DamianEdwards @Eilon @davidfowl
We should support some mechanism for eager (fail fast on startup) validation of options.
Needs to also work for generic host as well as webhost, must be configurable on a per options instance, since this will never work for request based options.
The text was updated successfully, but these errors were encountered: