Examples of IOptions<T>
validation in .NET Core
The Options pattern in .NET Core has a few different ways of validating configuration/options. The solutions in this repository attempt to explore the different approaches and the good/bad of each.
In all of the demonstrated .NET Core approaches, validation of the options object instance does not happen until the first usage.
IOptions<T>
: Validation only happens on the first access (even if the underlying configuration changes).IOptionsSnapshot<T>
: Validation happens on every new scope (usually every request).IOptionsMonitor<T>
: Runs validation on the next access after the underlying settings have changed.
For a discussion of which approach to use when injecting the options into a class, I recommend Andrew Lock's article. Keep in mind the object lifetimes for each approach and avoid captive dependencies (passing a scoped object like IOptionsSnapshot<T>
into the constructor of a singleton / long-lived object).
One approach to dealing with the lazy-evaluation of validation rules would be to add those IOptions<T>
as parameters to the Startup.Configure()
and then instantiate a copy of every options object. This would give you a way to validate that anything in the appsettings*.json
files (or environment variables) injected at startup are correct.
The main differences between the approaches to validation takes place in the AddValidatedSettings<T>()
or AddSettings<T>()
methods in the IServiceCollectionExtensions
class.
Because the DatabaseOptions
and other objects are passed into the WeatherForecastController
constructor, simply running the project after editing appsettings.json
file will let you experiment.
Uses the Microsoft Data Annotations approach of attribute-based validation on the C# model that represents the section in the configuration. This is the approach that I'd recommend for most use-cases as configuration validation is generally simple.
Note: Commit b1dac68 added support for recursively validating DataAnnotation attributes. The classes may have moved around in later commits, but the basic code remains the same.
- Pro: The data annotation approach gives back a full list of all validation that failed.
- Pro: Data annotation validation is simple to setup and works well for flat options.
- Con: Data annotation via attributes quickly gets complicated when you have fields relying on each other.
- Con: Data annotation validation does not validate sub-objects (without custom code).
Uses the .Validate()
method and custom validation methods on the C# classes. Note that use of a marker/trait interface is not required, but it made it easier for me to call the validation method from within a generic method. The use of a generic method makes it difficult to construct a detailed error message.
If I wasn't using a generic method (AddValidatedSettings
), it would be possible to separately validate each property of the Options object. This would allow per-property validation messages. An example of this can be seen in the old Aspnet/Options Github repository.
.Validate(o => o.Boolean)
.Validate(Options.DefaultName, o => o.Virtual == null, "Virtual")
.Validate(o => o.Integer > 12, "Integer");
I'm not a fan of this approach.
- Con: It is harder to structure a useful message / object back to the caller.
Uses the IValidateOptions
interface on C# validation classes. See the DatabaseOptionsValidator
class for an example validator. This was a really easy to implement approach.
What we found in practice is that doing validation like this is tedious and error-prone, but powerful. This approach could live along side the DataAnnotations appraoch in Example 1.
- Pro: Allows you to send back multiple string messages.
- Pro: Flexible / powerful.
- Con: Have to wire up each pair of options class and the associated validator.
- There is discussion about an "eager" validation routine in .NET Core, but it has not yet been added.
- The comment on StackOverflow by "poke" explains some of the trade-offs to consider when talking about configuration / options validation.
- Article about IOptions vs IOptionsSnapshot vs IOptionsMonitor by Andrew Lock and why you'll probably end up using
IOptionsMonitor<T>
.