Validate Options<T>
on Startup in .NET 8
The .NET 8 Host Builder allows you
to bind configuration with C# objects by using AddOptions<T>
and binding to the configuration.
It provides you an opportunity to validate the configuration values when the host (WebApplication or Hosted Server)
is starting by using ValidateOnStart
.
But there are two interesting aspects of it, which I will explain in this post.
Do you learn better by watching videos? Yes, I got you covered.
A detailed course on .NET configuration is available on YouTube. Remember to subscribe to the channel for more content.
Validate Complex Types
Let's say you have a configuration like this:
{
"Cities": [
{
"Name": "London",
"WeatherMood": "Sunny"
},
{
"Name": "Paris",
"WeatherMood": "Rainy"
},
{
"Name": "New York",
"WeatherMood": "Cloudy"
}
]
}
You can bind it to a C# object like this:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOptions<List<City>>()
.Bind(builder.Configuration.GetSection("Cities"))
.ValidateDataAnnotations()
.ValidateOnStart();
var app = builder.Build();
app.UseWelcomePage();
app.Run();
public class City
{
[Required] public string Name { get; set; }
[Required] public WeatherMood WeatherMood { get; set; }
}
public enum WeatherMood {
Sunny,
Rainy,
Cloudy
}
So far, so good. But what if you add weather mood as Snowy
in the configuration file. Will the application run?
{
"Cities": [
{
"Name": "London",
"WeatherMood": "Snowy"
},
{
"Name": "Paris",
"WeatherMood": "Rainy"
},
{
"Name": "New York",
"WeatherMood": "Cloudy"
}
]
}
Yes, but it will only have two cities in the list.
The third city is ignored because the weather
mood Snowy does not have a value in WeatherMood
enum.
Key is it is silently ignored, which can lead to a nasty bug because the item inside the array is ignored.
What is the fix number 1 with added perf?
You can fix it by adding the below flag in .csproj
file.
<PropertyGroup>
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>
It will add a source generator to your project to bind the configuration to the C# object.
Now it will throw an exception on startup. But also improves your startup time by generating the code at compile time and a good practice if you would like to use AOT compilation in the future.
What is the fix number 2?
You can configure how the binding should behave by using BinderOptions
and set the ErrorOnUnknownConfiguration
to true
.
When ErrorOnUnknownConfiguration
is true.
The configuration object or path or section, you pass to the Bind
method or BindConfiguration
extension method,
must have all the properties defined in the C# class.
Do not try to bind to the root configuration path because it includes all the configuration values including
the environment variables, CLI arguments, and other configuration providers.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOptions<List<City>>()
.Bind(builder.Configuration.GetSection("Cities"),
,options =>
{
//Bind non public properties - do not do it without a fantastic reason
//options.BindNonPublicProperties = true;
options.ErrorOnUnknownConfiguration = true;
})
.ValidateDataAnnotations()
.ValidateOnStart();
var app = builder.Build();
app.UseWelcomePage();
app.Run();
public class City
{
[Required] public string Name { get; set; }
[Required] public WeatherMood WeatherMood { get; set; }
}
public enum WeatherMood {
Sunny,
Rainy,
Cloudy
}
But what if you have validation attribute classes like [Required]
on the List Item, will those be validated?
No, even if you assign the city an empty string, so how to get those validated?
Apply [ValidateEnumeratedItems]
to validate array items
Create Config
class, and create a property named Cities
with [ValidateEnumeratedItems]
class,
and then bind the root configuration path
to the options.
Applying [ValidateEnumeratedItems]
is required to validate array items.
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddOptions<Config>()
.Bind(builder.Configuration)
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();
public class Worker(ILogger<Worker> logger, IOptions<Config> config) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
foreach (var city in config.Value.Cities)
{
logger.LogInformation("{CityName} is {CityWeatherMood}", city.Name, city.WeatherMood);
}
await Task.Delay(10000, stoppingToken);
}
}
}
public class Config
{
[ValidateEnumeratedItems] public List<City> Cities { get; set; }
}
public class City
{
[Required] public string Name { get; set; }
[Required] public WeatherMood WeatherMood { get; set; }
}
public enum WeatherMood
{
Sunny,
Rainy,
Cloudy
}
A Sample Repository Targeting NET6 is on GitHub. It will work with .NET 7 and .NET8 as well as .NET 6.
Feedback
I would love to hear your feedback, feel free to share it on Twitter.