In this post, let's have a look at how we can validate options in
application startup in a .NET application.
The Options pattern lets us bind a configuration section to a strongly-typed
class. On top of that, we can validate the bound values so that a
missing/incorrect configuration fails fast at application startup rather
than blowing up at some random point at runtime when the options are first
used. All of this lives in Microsoft.Extensions.Options, so it works
the same in Console apps, Worker Services, ASP.NET Core and any other .NET
host.
Let's have a look at a simple example.
Say we have the following appsettings.json.
{
"WeatherApi": {
"BaseUrl": "Something",
"TimeoutSeconds": 300,
"Cache": {
"DurationSeconds": 7200
}
}
}
And the following options classes and registration.
using Microsoft.Extensions.Options; using System.ComponentModel.DataAnnotations; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.Services.AddOptions<WeatherApiOptions>() .Bind(builder.Configuration.GetSection(WeatherApiOptions.SectionName)) .ValidateDataAnnotations() .ValidateOnStart(); // Omitted for brevity WebApplication app = builder.Build(); // Omitted for brevity app.Run(); public class WeatherApiOptions { public const string SectionName = "WeatherApi"; [Required] [Url] public string BaseUrl { get; set; } = string.Empty; [Range(1, 60)] public int TimeoutSeconds { get; set; } [Required] [ValidateObjectMembers] public CacheOptions Cache { get; set; } = new(); } public class CacheOptions { [Range(30, 3600)] public int DurationSeconds { get; set; } }
Here ValidateDataAnnotations() validates the DataAnnotation
attributes on our options type. By default though, that validation is
lazy, it only runs the first time someone accesses
IOptions<WeatherApiOptions>.Value. That means a misconfigured
application would happily start up and only fail later at runtime when the
options are first used. ValidateOnStart() fixes that by forcing the
validation to run eagerly at application startup, so we fail fast. (This
kicks in as long as something actually starts the host, e.g.
app.Run().)
Something to note is, the ValidateDataAnnotations() only
validates the top-level options type. It does not recurse into
nested objects (or into items of a collection). So the DataAnnotation
attributes on CacheOptions are silently ignored. And for that,
from .NET 8, onwards, two new attributes have been added to
Microsoft.Extensions.Options:
- [ValidateObjectMembers] - recursively validates the DataAnnotation attributes on a nested object.
- [ValidateEnumeratedItems] - recursively validates the DataAnnotation attributes on each item of a collection.
The application now fails at startup, and notice that all the
validation failures, including the ones on the nested object are reported at
once.
Microsoft.Extensions.Options.OptionsValidationException: DataAnnotation validation failed for 'WeatherApiOptions' members:
'BaseUrl' with the error: 'The BaseUrl field is not a valid fully-qualified http, https, or ftp URL.'.;
DataAnnotation validation failed for 'WeatherApiOptions' members:
'TimeoutSeconds' with the error: 'The field TimeoutSeconds must be between 1 and 60.'.;
DataAnnotation validation failed for 'WeatherApiOptions.Cache' members:
'DurationSeconds' with the error: 'The field DurationSeconds must be between 30 and 3600.'.
Hope this helps.
Happy Coding.
Regards,
Jaliya