blog post image
Andrew Lock avatar

Andrew Lock

~5 min read

Validating nested DataAnnotation IOptions recursively with MiniValidation

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.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?