Configuration Validation
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 ;)