In this short post I show how you can use the MiniValidation library from Damian Edwards to support recursive validation of IOptions
object in .NET 6+.
Validating IOptions
in .NET 6
Last year I wrote a post explaining how to add validation to your strongly typed IOptions
objects using built-in functionality and DataAnnotation
attributes. .NET 6 added support for this, as well as for validating on app startup, out-of-the-box. I suggest reading that post for details, but in summary it looks something like this:
builder.Services.AddOptions<MySettings>()
.BindConfiguration("MySettings") // π Bind the MySettings section in config
.ValidateDataAnnotations() // π Enable DataAnnotation validation
.ValidateOnStart(); // π Validate when the app starts
Inevitably, someone asked how you can do something similar with FluentValidation, so I wrote a post about that here. And of course, for completeness, someone else pointed out that ValidateDataAnnotations()
doesn't validate nested properties, so if you have something that looks like this:
public class MySettings
{
[Required]
public string DisplayName { get; set; }
[Required]
public NestedSettings Nested { get; set; }
public class NestedSettings
{
[Required]
public string Value { get; set; }
[Range(1, 100)]
public int Count { get; set; }
}
}
and you bind it as shown in the previous snippet, then the MySettings.Nested
property won't be validated. This often takes people by surprise, and is tracked in this issue, but fundamentally this stems from the way the Validator
in the DataAnnotation
namespace works - it doesn't recursively validate by default.
Until there's a solution in the box, there are plenty of workarounds to this, as described in the issue, but in this post I'm going to show a simple alternative: use the MiniValidation
NuGet package.
MiniValidation
MiniValidation is a small library created by Damian Edwards on the ASP.NET Core team. As per the README, MiniValidation is
A minimalistic validation library built atop the existing features in .NET's
System.ComponentModel.DataAnnotations
namespace. Adds support for single-line validation calls and recursion with cycle detection.
It contains code that I have often found myself writing in the past, to simplify using the built-in DataAnnotation
methods. It doesn't do anything especially fancy, but it does it in a nice little package without any effort! In this case, we're going to rely on it for the recursion it provides.
In it's simplest form, you can validate a DataAnnotations object mySettings
with a single call:
// π Do the validation errors is IDictionary<string, string[]>
MiniValidator.TryValidate(mySettings, out var errors);
// Print the errors
foreach((string member, string[] memberErrors) in errors)
{
var memberError = string.Join(", ", memberErrors);
Console.WriteLine($"{member} had the following errors: {memberError}");
}
It's a small thing, but the terseness and extra features (like recursion) make it an easy package to recommend playing with.
To demonstrate, in this post I'll show how you can hook it into the OptionsBuilder
to solve the problem with nested classes.
Validating IOptions
with MiniValidation
As I described in my previous post that used FluentValidation, the interface we need to implement is IValidateOptions<T>
. In the FluentValidation we also had to hook up various extra validators, but in this case it's simpler as we're just reusing the existing DataAnnotation
attributes.
To start with, you'll need to install the MiniValidation library. From the command line, run
dotnet add package MinimalApis.Extensions
This adds the package to your .csproj file, something like this:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<!-- π Add this -->
<PackageReference Include="MiniValidation" Version="0.7.4" />
</ItemGroup>
</Project>
Now we can create an IValidateOptions<T>
implementation that uses the MiniValidation helpers. The implementation below is based on the original DataAnnotationValidateOptions
type used in the ValidateDataAnnotations()
call, so you can consider it a complete implementation.
This implementation handles named options too. If you're not familiar with named options, you can read my post about them here.
public class MiniValidationValidateOptions<TOptions>
: IValidateOptions<TOptions> where TOptions : class
{
public MiniValidationValidateOptions(string? name)
{
Name = name;
}
public string? Name { get; }
public ValidateOptionsResult Validate(string? name, TOptions options)
{
// Null name is used to configure ALL named options, so always applys.
if (Name != null && Name != name)
{
// Ignored if not validating this instance.
return ValidateOptionsResult.Skip;
}
// Ensure options are provided to validate against
ArgumentNullException.ThrowIfNull(options);
// π MiniValidation validation π
if (MiniValidator.TryValidate(options, out var validationErrors))
{
return ValidateOptionsResult.Success;
}
string typeName = options.GetType().Name;
var errors = new List<string>();
foreach (var (member, memberErrors) in validationErrors)
{
errors.Add($"DataAnnotation validation failed for '{typeName}' member: '{member}' with errors: '{string.Join("', '", memberErrors)}'.");
}
return ValidateOptionsResult.Fail(errors);
}
}
To make it easy to add this type to your options we'll create an extension method on OptionsBuilder
too:
public static class MiniValidationExtensions
{
public static OptionsBuilder<TOptions> ValidateMiniValidation<TOptions>(
this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class
{
// π register the validator against the options
optionsBuilder.Services.AddSingleton<IValidateOptions<TOptions>>(
new MiniValidationValidateOptions<TOptions>(optionsBuilder.Name));
return optionsBuilder;
}
}
And that's it, we're done!
Trying it out
To use our new extension, we simply replace the call to ValidateDataAnnotations()
with ValidateMiniValidation()
. No other changes are needed in our project:
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
using MiniValidation;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOptions<MySettings>()
.BindConfiguration("MySettings")
.ValidateMiniValidation() // π Replace with mini validation
.ValidateOnStart();
var app = builder.Build();
app.MapGet("/", (IOptions<MySettings> options) => options.Value);
app.Run();
If we have the following config in our appsettings.json file (where the Nested
property values are invalid):
{
"MySettings": {
"DisplayName": "Display",
"Nested" : {
"Count": 0
}
}
}
then when we run our application, we'll get an error when you call app.Run()
:
OptionsValidationException:
DataAnnotation validation failed for 'MySettings' member: 'Nested.Value' with errors: 'The Value field is required.'.;
DataAnnotation validation failed for 'MySettings' member: 'Nested.Count' with errors: 'The field Count must be between 1 and 100.'.
Microsoft.Extensions.Options.OptionsFactory<TOptions>.Create(string name)
Microsoft.Extensions.Options.OptionsMonitor<TOptions>+<>c__DisplayClass10_0.<Get>b__0()
...
Success!
This is just one of the ways you can handle recursive validation in your IOptions
object, but it feels like an easy one to me!
Summary
In this post I described the problem that by default, DataAnnotation validation doesn't recursively inspect all properties in an object for DataAnnotation attributes. There are several solutions to this problem, but in this post I used the MiniValidation library from Damian Edwards. This simple library provides a convenience wrapper around DataAnnotation validation, as well as providing features like recursive validation. Finally I showed how you can replace the built-in DataAnnotation validation with a MiniValidation-based validator.