Validating appsettings becomes much faster with .NET 8

Published: Tuesday 19 December 2023

.NET 8 has introduced a faster way to validate option values from the appsettings.json configuration.

These values can either be validated on startup or through runtime.

C# coding challenges

C# coding challenges

Our online code editor allows you to compile the answer.

We'll explain what's changed, why it's faster and how to set it up.

Validating options prior to .NET 8

The appsettings.json file has the flexibility to add a number of different configuration options.

In this example, we have added a AgeRestriction section to appsettings.json with values for the MinimumAge and MaximumAge.

{
  ...,
  "AgeRestriction": {
    "MinimumAge": 18,
    "MaximumAge": 44
  }
}

These values have been added as properties to an AgeRestrictionOptions class. We've added data annotation attributes to each one for validation. Both of them are required and need to be within a range of 18-65.

// AgeRestrictionOptions.cs
public class AgeRestrictionOptions
{
	[Required]
	[Range(18, 65)]
	public int MinimumAge { get; set; }

	[Required]
	[Range(18, 65)]
	public int MaximumAge { get; set; }
}

To bind the appsettings.json values to the AgeRestrictionOptions class in an ASP.NET Core app, we need to add some addition configuration options to the Program.cs file.

// Program.cs
var builder = WebApplication.CreateBuilder(args);

...

builder.Services.AddOptions<AgeRestrictionOptions>()
    .BindConfiguration("AgeRestriction")
    .ValidateDataAnnotations()
    .ValidateOnStart(); 	

...

var app = builder.Build();

...

The AddOptions method requires a generic type which is the class that we bind our appsettings value to. The BindConfiguration method specifies the section in the appsettings.json where we are getting the values from.

There are also two validate methods that we use. The ValidateDataAnnotations method ensures that the options are validating when they are used. An additional ValidateOnStart method ensures that these options are validated before the application is started.

Taking a performance hit

It must be noted that using these methods to bind and validate configuration options with a .NET 8 project will work perfectly fine.

But the validation does take a performance hit as it uses reflection. Depending on how complex the validation is will determine how much of a performance hit an application will take.

There is a different way of doing this with .NET 8. It involves creating a new partial class which will generate code needed for the validation.

Validating options with a code-generated class

The first step is to create an options validator class. This needs to be marked as partial as there will be another partial class that will be code generated.

In-addition, the class needs to be implemented with the IValidateOptions interface, passing in the AgeRestrictionOptions class as it's generic type.

At this stage, the code will not compile as the IValidateOptions interface is expecting a Validate method.

However, by adding the [OptionsValidator] attribute to the class, it resolves the compile issues.

// AgeRestrictionOptionsValidator.cs
[OptionsValidator]
public partial class AgeRestrictionOptionsValidator 
: IValidateOptions<AgeRestrictionOptions>
{
}

Code generation

At this point when we build the application, it generates another partial AgeRestrictionOptionsValidator class. 

This includes a Validate method which contains the code to validate options.

partial class AgeRestrictionOptionsValidator
{
    /// <summary>
    /// Validates a specific named options instance (or all when <paramref name="name"/> is <see langword="null" />).
    /// </summary>
    /// <param name="name">The name of the options instance being validated.</param>
    /// <param name="options">The options instance.</param>
    /// <returns>Validation result.</returns>
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "8.0.9.3103")]
    [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode",
         Justification = "The created ValidationContext object is used in a way that never call reflection")]
    public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::RoundTheCode.OptionsValidation.Options.AgeRestrictionOptions options)
    {
        ...
    }
}

Configuration in Program.cs

With the options validator setup, we need to make some additional changes to the configuration in Program.cs.

First, the original configuration needs to be removed and replaced with the following:

// Program.cs
using Microsoft.Extensions.Options;
using RoundTheCode.OptionsValidation.Options;

var builder = WebApplication.CreateBuilder(args);

...

/* This needs to be removed
builder.Services.AddOptions<AgeRestrictionOptions>()
    .BindConfiguration("AgeRestriction")
    .ValidateDataAnnotations()
    .ValidateOnStart(); 	
*/

/* Add this */
builder.Services.Configure<AgeRestrictionOptions>(
    builder.Configuration.GetSection("AgeRestriction")
);

...

var app = builder.Build();

...

This will bind the values from appsettings.json to the AgeRestrictionOptions class, but it will not validate it.

In-order to validate it, we need to add the AgeRestrictionOptionsValidator class with a singleton service lifetime to the IoC container.

// Program.cs
using Microsoft.Extensions.Options;
using RoundTheCode.OptionsValidation.Options;

var builder = WebApplication.CreateBuilder(args);

...

/* Add this */
builder.Services.Configure<AgeRestrictionOptions>(
    builder.Configuration.GetSection("AgeRestriction")
);

builder.Services.AddSingleton<IValidateOptions<AgeRestrictionOptions>, AgeRestrictionOptionsValidator>();

...

var app = builder.Build();

...

This will now validate on runtime whenever the options are called.

What about validating on startup?

At present, there is no documented way of validating appsettings.json values through code generation in .NET 8.

However, there is a workaround.

When the web application is built in Program.cs, we have access to the IServiceProvider instance. As a result, we can resolve instances that are contained in the IoC container.

// Program.cs
using Microsoft.Extensions.Options;
using RoundTheCode.OptionsValidation.Options;

var builder = WebApplication.CreateBuilder(args);

...

var app = builder.Build(); // This builds the application and we have access to the IServiceProvider instance

...

That means we can get a reference to the options and options validator before the application is run.

using Microsoft.Extensions.Options;
using RoundTheCode.OptionsValidation.Options;

var builder = WebApplication.CreateBuilder(args);

...

var app = builder.Build(); // This builds the application and we have access to the IServiceProvider instance

// Validate on startup
var ageRestrictionOptions = app.Services.GetRequiredService<IOptions<AgeRestrictionOptions>>();
var ageRestrictionOptionsValidator = app.Services.GetRequiredService<IValidateOptions<AgeRestrictionOptions>>();

...

app.Run();

When creating the options validator, it generated a partial class of the same name with a Validate method. This method requires two parameters:

  • The name parameter specifies the property name to validate. This can be specified as NULL if we want to validate all properties.
  • The options parameter is essentially the values from the appsettings.json file stored in the AgeRestrictionOptions class.

As a result, we can call the Validate method before the application is running and it will throw an exception if it does not validate properly.

// Program.cs
using Microsoft.Extensions.Options;
using RoundTheCode.OptionsValidation.Options;

var builder = WebApplication.CreateBuilder(args);

...

var app = builder.Build(); // This builds the application and we have access to the IServiceProvider instance

// Validate on startup
var ageRestrictionOptions = app.Services.GetRequiredService<IOptions<AgeRestrictionOptions>>();
var ageRestrictionOptionsValidator = app.Services.GetRequiredService<IValidateOptions<AgeRestrictionOptions>>();

ageRestrictionOptionsValidator.Validate(null, ageRestriction.Value); // Validates before the application is run.

...

app.Run();

Watch the video

Watch our video where we implement the previous way of setting up validation on configuration values in an ASP.NET Core Web API and what changes we need to make to set it up using code generation.