I've spoken previously about IOptions in ASP.NET core. On their own, they are a very useful way to inject configuration into your services. What I didn't mention is that they allow you to build in validation of your configuration. One reason that this might be needed is that the IOptions services are normally very forgiving on missing configuration. If you don't have a value in your appsettings.json then it will just populate with a default value. This could cause serious hidden errors at runtime for your application.
The easiest way to validate configuration is with attributes. You can add the Required attribute to your POCO class and configure it in the Startup ConfigureServices like this:
services.AddOptions<Person>()
.ValidateDataAnnotations();
Then you can decorate the class with any of the System.ComponentModel.DataAnnotations validations.
The below code will make sure Name is required and that Company has a maximum of 5 characters.
public class Person
{
[Required]
public String Name { get; set; }
[StringLength(5, ErrorMessage = Company short name only)]
public String Company { get; set; }
public int Age { get; set; }
}
When applied, you will get an exception when you access the IOptions Value if validation fails. So this doesn't occur at startup but instead when you try access the configuration value.
Another way would be to do the validation in the Startup registration. For example with an inline method:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
bool ValidatePerson(Person p)
{
return p.Age > 0;
}
services.AddOptions<Person>()
.Validate((p) => ValidatePerson(p));
services.AddHostedService<PersonService>();
}
Disclaimer: This is obviously just test code and I don't recommend using inline methods in your ConfigureServices as it will start getting very large very quickly.
The last way available is to implement the IValidateOptions interface and register it against your option. This is the heaviest option but it allows you to register multiple validation classes against your option and you can inject other services into the validation to help with more complex rules.
Your startup with a single validation:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<IValidateOptions<Person>, PersonValidationName>();
services.AddOptions<Person>();
services.AddHostedService<PersonService>();
}
and the validation:
public class PersonValidationName : IValidateOptions<Person>
{
public PersonValidationName(IOptions<OtherOption> other)
{
}
public ValidateOptionsResult Validate(string name, Person options)
{
return string.IsNullOrWhiteSpace(name) ?
ValidateOptionsResult.Fail("Name is missing") :
ValidateOptionsResult.Success;
}
}
This will work the same as calling the validate method.