ASP.NET Core Configuration â Options Validation
ASP.NET Core Configuration â Options Validation êŽë š
In this article, weâre going to learn the importance of options validation and a few ways to implement it in our ASP.NET Core applications. With advanced mechanisms like configuration reloading, we need to think about options validation carefully as we donât want to cause application crashes or unexpected behavior.
Just as a reminder, in the previous article, weâve talked about the options interfaces and how to implement them. Now we need to protect our application from invalid configuration values.
Whether we work on a large project with many configuration parameters, or we rely on external configuration providers, validation plays an important role in an application lifecycle.
Info
The source code for this article can be found on the ASP.NET Core Configuration repo on GitHub (CodeMazeBlog/aspnet-core-configuration
). If you wish to follow along, use the options-pattern
(CodeMazeBlog/aspnet-core-configuration
) branch. To check out the finished source code, check out the options-validation
(CodeMazeBlog/aspnet-core-configuration
) branch.
Letâs dive in.
Options Validation with DataAnnotations
One way to do validation of configuration parameters is by using DataAnnotations from System.ComponentModel.DataAnnotations
. You might have seen data annotations used in other scenarios like validating forms in ASP.NET Core MVC or Blazor.
But we can use them for validating our configuration on application start or configuration reload.
Letâs add some data annotations to our model TitleConfiguration
:
public class TitleConfiguration
{
[Required]
[MaxLength(60)]
public string WelcomeMessage { get; set; }
public bool ShowWelcomeMessage { get; set; }
public string Color { get; set; }
public bool UseRandomTitleColor { get; set; }
}
Weâve added both [Required]
and [MaxLength]
attributes, so we can state that our WelcomeMessage
is mandatory and that it isnât longer than 60 characters.
Now that weâve decorated our WelcomeMessage
property, we need to configure our validator in order to check these values.
In the previous part, weâve configured our options in the Startup
class:
services.Configure<TitleConfiguration>("HomePage", Configuration.GetSection("Pages:HomePage"));
We can remove that line because we need to do it a bit differently in order to enable configuration validation:
services.AddOptions<TitleConfiguration>()
.Bind(Configuration.GetSection("Pages:HomePage"))
.ValidateDataAnnotations();
Weâre using the AddOptions()
method to add the configuration and the Bind()
method to bind it to a specific configuration subsection, in our case âPages:HomePageâ. After that, we can call ValidateDataAnnotations()
method to make sure our validation triggers for the data annotations weâve set.
We can also quickly revert TitleService
, ITitleService
, and HomeController
to not use the named options, since we donât need them anymore:
public class TitleColorService : ITitleColorService
{
private readonly string[] _colors = { "red", "green", "blue", "black", "purple", "yellow", "brown", "pink" };
private readonly TitleConfiguration _titleConfiguration;
public TitleColorService(IOptionsMonitor<TitleConfiguration> titleConfiguration)
{
_titleConfiguration = titleConfiguration.CurrentValue;
}
public string GetTitleColor()
{
var random = new Random();
var colorIndex = random.Next(7);
return _titleConfiguration.UseRandomTitleColor ?
_colors[colorIndex] :
_titleConfiguration.Color;
}
}
public interface ITitleColorService
{
string GetTitleColor();
}
public HomeController(ILogger<HomeController> logger,
IOptionsSnapshot<TitleConfiguration> homePageTitleConfiguration,
ITitleColorService titleColorService)
{
_logger = logger;
_homePageTitleConfiguration = homePageTitleConfiguration.Value;
_titleColorService = titleColorService;
}
public IActionResult Index()
{
var homeModel = new HomeModel
{
Configuration = _homePageTitleConfiguration
};
homeModel.Configuration.Color = _titleColorService.GetTitleColor();
return View(homeModel);
}
Now letâs head back to our appsettings.json and remove the WelcomeMessage
option (we can remove the âProductPageâ section altogether too since we wonât need it):
"Pages": {
"HomePage": {
"ShowWelcomeMessage": true,
"Color": "black",
"UseRandomTitleColor": true
}
},
Sure enough, if we run the application now, weâll get OptionsValidationException
:
Moreover, weâll get the details of the field that was problematic, and thatâs WelcomeMessage
in our case.
We can also try the MaxLength validation by adding a few words to the WelcomeMessage
option:
"Pages": {
"HomePage": {
"WelcomeMessage": "Hi human, and welcome to the ProjectConfigurationDemo Home Page",
"ShowWelcomeMessage": true,
"Color": "black",
"UseRandomTitleColor": true
}
},
Now we get the maximum length exceeded exception:
Great, works, like a charm.
And not only that, but it works even if we change the configuration and correct it whilst the application is running. Try it out! Revert the WelcomeMessage
to the valid value and refresh the page to see what happens.
Needless to say, thatâs fantastic stuff.
But what if we need a more flexible validation logic?
Options Validation using Delegates
Another great way to configure our options validation is by using delegates. The fastest way to do it is by using an anonymous function inside the Validate()
method thatâs an extension of the OptionsBuilder
weâve used previously:
services.AddOptions<TitleConfiguration>()
.Bind(Configuration.GetSection("Pages:HomePage"))
.ValidateDataAnnotations()
.Validate(config =>
{
if (string.IsNullOrEmpty(config.WelcomeMessage) || config.WelcomeMessage.Length > 60)
return false;
return true;
});
Now we can remove ValidateDataAnnotations()
and the data annotations themselves from the TitleConfiguration
class. As you can see this way is much more flexible and we can do whatever custom stuff we want.
Now if we comment out WelcomeMessage and run the application again we get a bit different kind of exception:
This message is a bit generic, but thatâs to be expected since weâre doing our own custom validation logic. We can make it a bit better by defining a failure message:
services.AddOptions<TitleConfiguration>()
.Bind(Configuration.GetSection("Pages:HomePage"))
.Validate(config =>
{
if (string.IsNullOrEmpty(config.WelcomeMessage) || config.WelcomeMessage.Length > 60)
return false;
return true;
}, "Welcome message must be defined and it must be less than 60 characters long.");
Now our exception shows this message:
Of course, we can do some pretty nice stuff with delegates, so if youâre not familiar with delegates that much check out our article about delegates in C#.
Great, we learned how to do custom validation if needed. With these methods, weâre able to implement validation quickly.
But letâs see what we can do in those really complex validation scenarios.
Complex Validation Scenarios with IValidateOptions
For more complex validation scenarios, which arenât that rare, we can use the IValidateOptions
interface to move our validation logic out of the Startup class, and into its own separate class.
In order to do that, letâs create a TitleConfigurationValidation
class first and implement IValidateOptions
interface. For that purpose, we can create a separate folder called ConfigurationValidation
and then create a new class TitleConfigurationValidation
inside it:
public class TitleConfigurationValidation : IValidateOptions<TitleConfiguration>
{
private readonly TitleConfiguration _titleConfiguration;
public ValidateOptionsResult Validate(string name, TitleConfiguration options)
{
throw new NotImplementedException();
}
}
We are going to implement the IValidateOptions
interface using the TitleConfiguration
as the options parameter for the Validate()
method.
The IValidateOptions
interface declares only one method â Validate()
, and it accepts two arguments, name and options. We can also see that this method returns ValidateOptionsResult
which is a convenient way to provide result information. Much more convenient than just true or false like we did previously.
First, we can move our existing options validation we did in the Startup class to this method:
public ValidateOptionsResult Validate(string name, TitleConfiguration options)
{
if (string.IsNullOrEmpty(options.WelcomeMessage) || options.WelcomeMessage.Length > 60)
return ValidateOptionsResult.Fail("Welcome message must be defined and it must be less than 60 characters long.");
return ValidateOptionsResult.Success;
}
That certainly looks better. We can now clean up the Startup
class, and register our TitleConfigurationValidation
as a singleton:
{
services.Configure<TitleConfiguration>(Configuration.GetSection("Pages:HomePage"));
services.TryAddSingleton<IValidateOptions<TitleConfiguration>, TitleConfigurationValidation>();
services.TryAddSingleton<ITitleColorService, TitleColorService>();
services.AddControllersWithViews();
}
If we run the application again, we should get the same result as we did previously.
Now letâs show off the full power of the IValidateOptions
interface by implementing the title color validation. Say, for example, we want to make sure that the title color is just among the colors we provided in order to make our application look consistent.
We just need to extend our Validation class to support this logic:
public class TitleConfigurationValidation : IValidateOptions<TitleConfiguration>
{
private readonly string[] _colors = { "red", "green", "blue", "black", "purple", "yellow", "brown", "pink" };
public ValidateOptionsResult Validate(string name, TitleConfiguration options)
{
if (string.IsNullOrEmpty(options.WelcomeMessage) || options.WelcomeMessage.Length > 60)
return ValidateOptionsResult.Fail("Welcome message must be defined and it must be less than 60 characters long.");
if (!_colors.Any(c => c == options.Color))
return ValidateOptionsResult.Fail($"Provided title color '{options.Color}' is not among allowed colors.");
return ValidateOptionsResult.Success;
}
}
Now the title color must be among those weâve provided. If we provide different color, for example, gray:
"Pages": {
"HomePage": {
"WelcomeMessage": "Welcome to the ProjectConfigurationDemo Home Page",
"ShowWelcomeMessage": true,
"Color": "gray",
"UseRandomTitleColor": true
}
},
After running the application weâll get a message saying âgrayâ is not valid color:
But it gets even better, this approach also supports the configuration reloading, so you can simply revert the color to âblackâ and refresh the page to get it working again.
Great! Weâve learned how to validate our configuration in several different ways.
Conclusion
In this article, weâve covered options validation in three different ways. The first way is by using DataAnnotations, which is a common way of validating fields in ASP.NET Core, weâve seen how to configure it by using delegates, and finally, weâve learned how to use IValidateOptions
to validate complex scenarios and clean up our code.
In the next part, weâre going to learn more about different configuration providers, and you can find other parts of this series on the ASP.NET Core Web API page.