Configuration Validation
Configuration can be challenging in ASP.NET. There can be multiple sources and it may be a challenge to predict exactly what configuration values your system works with. Especially when this system runs in a (cloud)production environment that cannot be accessed easily. Adding validation to your configuration may help you get your system up and running in a healthy state, or at least provide you with some meaningful errors when your configuration contains errors.

A while ago I wrote a blog post about configuration in ASP.NET. How it works and how the system comes to the final configuration of the system. This particular post focuses on configuration specifically for container apps, but the greater idea still stands for every system you create using ASP.NET.

This post handles the configuration validation. Because often, when you F5 your project and run it on your local machine everything runs fine. Then you deploy it and it suddenly won’t start. Good chance there is a configuration issue. These issues are hard to find because since your system didn’t start up properly, chances are that your logs and telemetry ingestion are not configured and up and running. Where do you find these errors to find the cause of the problem? A nice and clear message that explains what’s wrong and how you can fix it would be nice!

The project used for this example uses two packages that can be referenced in NuGet:

  • Microsoft.Extensions.Options
  • Microsoft.Extensions.Options.ConfigurationExtensions

With this blog post, I want to explain how you can add validation to your configuration, and execute that validation when desired so you can make sure your configuration is all ready to go whenever and wherever you need it in your system.

The Options Pattern

First of all, if you run an ASP.NET project, I strongly recommend using the Options pattern for configuration. Your configuration (or sections of it) are mapped to strongly typed classes. You can now add this strongly typed class to your dependency injection system and pull it out wherever you need it.

Let’s say that you have some configuration for a system where you need to pull a connection string and a secret value from the configuration. With the options pattern, you would start to create a new class containing a ConnectionString, and a Secret property.

public class ConfigurableValues
{
    public string ConnectionString { get; }
    public string Secret { get; }
}

Personally, I think that it is a good idea to organize your configuration settings and thus organize these settings in a section so that it is clear that they belong to each other. Sections in your app settings JSON file are represented by a JSON object. This object has a name and I think it’s a good idea to include that name in the strongly typed class like so:

public class ConfigurableValues
{
    public const string SectionName = "CustomConfig";

    public string ConnectionString { get; }
    public string Secret { get; }
}

The class ConfigurableValues obviously should be renamed to something more meaningful. ConfiguableValues in this example does not describe the configured values ;)

Configuring the system

Now in your ASP.NET Startup class, you would add a line that looks similar to this:

builder.Services.AddOptions<ConfigurableValues>()
    .Bind(builder.Configuration.GetSection(ConfigurableValues.SectionName));

This line binds the configured values in the section named “CustomConfig” of your app settings to the ConfigurableValues class and adds this class to the service collection of your Dependency Injection (DI) system.

You can open the appsettings.json file and create an object called “CustomConfig” to add to elements with values, however… Connection strings and secrets should be treated as secrets and therefore not added to appsettings.json. This settings file is potentially pushed to your source control system or shared in different ways, so secrets in these files should be considered compromised. Alternatively, right-click your project in Visual Studio and select Manage User Secrets. This will open a new JSON file called secrets.json that will remain in a secure place on your machine, and not end up in your source control system. The values will overwrite values in your appsettings.json file (again, if you want to know why, I refer to the blog post I wrote earlier).

With the secrets.json file open, write a JSON object that looks like this:

"CustomConfig": {
  "ConnectionString":  "Secret connection string",
  "Secret": "This is a secret value"
}

Your system is now configured and ready to go…

Reading configuration wherever you need it

Again, the options pattern in ASP.NET takes advantage of the DI system in ASP.NET. This means that you can now pull the strongly typed class from the DI system, for example using Constructor Injection, in classes wherever you need it. This example uses the WeatherForecast project template used when creating a new ASP.NET Web API project. Open up the WeatherForecastController and and modify it like so:

private readonly IOptions<ConfigurableValues> _configurationValues;

public WeatherForecastController(
    ILogger<WeatherForecastController> logger,
    IOptions<ConfigurableValues> configurationValues)
{
    _logger = logger;
    _configurationValues = configurationValues;
}

So add a read-only field of type IOptions and whatever name suits you best. In the constructor, add a parameter of the same type and assign the parameter value to the field you just created.

That’s it. You can now use the _configurableValues.Value property to read values from your configuration.

Adding validation

To add validation, create a new class. I named the class the same as the strongly typed class that contains my configuration and add Validator to the name. So in this example ConfigurableValuesValidator. This is not mandatory but describes the purpose of the class. The class looks like so:

public class ConfigurableValuesValidator : IValidateOptions<ConfigurableValues>
{
    public ValidateOptionsResult Validate(string? name, ConfigurableValues options)
    {
        var errorList = new List<string>();
        if (string.IsNullOrWhiteSpace(options.Secret))
        {
            errorList.Add($"The app setting {ConfigurableValues.SectionName}.Secret cannot be null or empty");
        }
        return errorList.Count > 0 ? ValidateOptionsResult.Fail(errorList) : ValidateOptionsResult.Success;
    }
}

Note that the class implements the IValidateOptions interface. This interface requires implementing the Validate() function, where you can add your desired validation code. In this case, I created a list of strings so I can add more errors at once. This is more user-friendly compared to throwing errors one at a time. For this example, the Secret value is mandatory and as you can see in the code, I validate that using the string.IsNullOrWhitespace() function. Finally, the function returns a ValidateOptionsResult, depending on whether or not there are errors on the errorList or not.

To make the system execute this validation function, you must also add the Validator class to the DI system in the Program.cs file. Under the line where you injected the ConfigurableValues options, add the following line:

builder.Services
    .TryAddSingleton<IValidateOptions<ConfigurableValues>,
        ConfigurableValuesValidator>();

This will tell the system, that there is a validator for the ConfigurableValues options. That’s all there is to it… Now when you change your configuration values, so again right-click on your project in Visual Studio and choose Manage User Secrets to open the secrets.json file. Remove the Secret value, just to test the validation.

Now start your system and send a request to a controller that gets the IOptions<ConfigurableValues> injected. You will see that the code now fails and shows the error you configured in the Validator.

Configuration at startup

To be honest, I think the configuration validation is charming, but… Waiting for the validation to be executed whenever the values are first used feels somewhat… not right? I would like the configuration to be validated at startup. Maybe there are some optional components for which the configuration validation can wait, but in general, I think configuration validation must be executed at startup. Luckily, you’re almost there with the code created throughout this article. You only need some adjustments in the Program.cs file where you add the ConfigurableValues object:

builder.Services.AddOptions<ConfigurableValues>()
    .Bind(builder.Configuration.GetSection(ConfigurableValues.SectionName))
    .ValidateOnStart();

That’s it! Your configuration is now validated and your validation code will run immediately at startup. Congratulations and job well done ;)


Last modified on 2023-01-24

Hi, my name is Eduard Keilholz. I'm a Microsoft developer working at 4DotNet in The Netherlands. I like to speak at conferences about all and nothing, mostly Azure (or other cloud) related topics.