Validation of ASP.NET Core 2.x options during startup
There is no real way to run a configuration validation during startup. As you already noticed, post configure actions run, just like normal configure actions, lazily when the options object is being requested. This completely by design, and allows for many important features, for example reloading configuration during run-time or also options cache invalidation.
What the post configuration action is usually being used for is not a validation in terms of “if there’s something wrong, then throw an exception”, but rather “if there’s something wrong, fall back to sane defaults and make it work”.
For example, there’s a post configuration step in the authentication stack, that makes sure that there’s always a SignInScheme
set for remote authentication handlers:
options.SignInScheme = options.SignInScheme ?? _authOptions.DefaultSignInScheme ?? _authOptions.DefaultScheme;
As you can see, this will not fail but rather just provides multiple fallbacks.
In this sense, it’s also important to remember that options and configuration are actually two separate things. It’s just that the configuration is a commonly used source for configuring options. So one might argue that it is not actually the job of the options to validate that the configuration is correct.
As such it might make more sense to actually check the configuration in the Startup, before configuring the options. Something like this:
var myOptionsConfiguration = Configuration.GetSection("MyOptions");
if (string.IsNullOrEmpty(myOptionsConfiguration["Url"]))
throw new Exception("MyOptions:Url is a required configuration");
services.Configure<MyOptions>(myOptionsConfiguration);
Of course this easily becomes very excessive, and will likely force you to bind/parse many properties manually. It will also ignore the configuration chaining that the options pattern supports (i.e. configuring a single options object with multiple sources/actions).
So what you could do here is keep your post configuration action for validation, and simply trigger the validation during startup by actually requesting the options object. For example, you could simply add IOptions<MyOptions>
as a dependency to the Startup.Configure
method:
public void Configure(IApplicationBuilder app, IOptions<MyOptions> myOptions)
{
// all configuration and post configuration actions automatically run
// …
}
If you have multiple of these options, you could even move this into a separate type:
public class OptionsValidator
{
public OptionsValidator(IOptions<MyOptions> myOptions, IOptions<OtherOptions> otherOptions)
{ }
}
At that time, you could also move the logic from the post configuration action into that OptionsValidator
. So you could trigger the validation explicitly as part of the application startup:
public void Configure(IApplicationBuilder app, OptionsValidator optionsValidator)
{
optionsValidator.Validate();
// …
}
As you can see, there’s no single answer for this. You should think about your requirements and see what makes the most sense for your case. And of course, this whole validation only makes sense for certain configurations. In particular, you will have difficulties when working configurations that will change during run-time (you could make this work with a custom options monitor, but it’s probably not worth the hassle). But as most own applications usually just use cached IOptions<T>
, you likely don’t need that.
As for PostConfigure
and PostConfigureAll
, they both register an IPostConfigure<TOptions>
. The difference is simply that the former will only match a single named option (by default the unnamed option—if you don’t care about option names), while PostConfigureAll
will run for all names.
Named options are for example used for the authentication stack, where each authentication method is identified by its scheme name. So you could for example add multiple OAuth handlers and use PostConfigure("oauth-a", …)
to configure one and PostConfigure("oauth-b", …)
to configure the other, or use PostConfigureAll(…)
to configure them both.
On an ASP.NET Core 2.2 project I got this working doing eager validation by following these steps...
Given an Options class like this one:
public class CredCycleOptions
{
[Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
public int VerifiedMinYear { get; set; }
[Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
public int SignedMinYear { get; set; }
[Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
public int SentMinYear { get; set; }
[Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
public int ConfirmedMinYear { get; set; }
}
In Startup.cs
add these lines to ConfigureServices
method:
services.AddOptions();
// This will validate Eagerly...
services.ConfigureAndValidate<CredCycleOptions>("CredCycle", Configuration);
ConfigureAndValidate
is an extension method from here.
public static class OptionsExtensions
{
private static void ValidateByDataAnnotation(object instance, string sectionName)
{
var validationResults = new List<ValidationResult>();
var context = new ValidationContext(instance);
var valid = Validator.TryValidateObject(instance, context, validationResults);
if (valid)
return;
var msg = string.Join("\n", validationResults.Select(r => r.ErrorMessage));
throw new Exception($"Invalid configuration for section '{sectionName}':\n{msg}");
}
public static OptionsBuilder<TOptions> ValidateByDataAnnotation<TOptions>(
this OptionsBuilder<TOptions> builder,
string sectionName)
where TOptions : class
{
return builder.PostConfigure(x => ValidateByDataAnnotation(x, sectionName));
}
public static IServiceCollection ConfigureAndValidate<TOptions>(
this IServiceCollection services,
string sectionName,
IConfiguration configuration)
where TOptions : class
{
var section = configuration.GetSection(sectionName);
services
.AddOptions<TOptions>()
.Bind(section)
.ValidateByDataAnnotation(sectionName)
.ValidateEagerly();
return services;
}
public static OptionsBuilder<TOptions> ValidateEagerly<TOptions>(this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class
{
optionsBuilder.Services.AddTransient<IStartupFilter, StartupOptionsValidation<TOptions>>();
return optionsBuilder;
}
}
I plumbed ValidateEargerly
extension method right inside ConfigureAndValidate
. It makes use of this other class from here:
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);
};
}
}
This allows us to add data annotations to the CredCycleOptions
and get nice error feedback right at the moment the app starts making it an ideal solution.
If an option is missing or have a wrong value, we don't want users to catch these errors at runtime. That would be a bad experience.
This NuGet package provides a ConfigureAndValidate<TOptions>
extension method which validates options at startup using an IStartupFilter
.
It is based on based on Microsoft.Extensions.Options.DataAnnotations. But unlike Microsoft's package, it can even validate nested properties. It is compatible with .NET Core 3.1 and .NET 5.
Documentation & source code (GitHub)
Andrew Lock explains options validation with IStartupFilter
.