In this post I describe an approach you can use to ensure your strongly typed configuration objects have been correctly bound to your configuration when your app starts up. By using an IStartupFilter
, you can validate that your configuration objects have expected values early, instead of at some point later when your app is running.
I'll start by giving some background on the configuration system in ASP.NET Core and how to use strongly typed settings. I'll briefly touch on how to remove the dependency on IOptions
, and then look at the problem I'm going to address - where your strongly typed settings are not bound correctly. Finally, I'll provide a solution for the issue, so you can detect any problems at app startup.
Strongly typed configuration in ASP.NET Core
The configuration system in ASP.NET Core is very flexible, allowing you to load configuration from a wide range of locations: JSON files, YAML files, environment variables, Azure Key Vault, and may others. The suggested approach to consuming the final IConfiguration
object in your app is to use strongly typed configuration.
Strongly typed configuration uses POCO objects to represent a subset of your configuration, instead of the raw key-value pairs stored in the IConfiguration
object. For example, maybe you're integrating with Slack, and are using Webhooks to send messages to a channel. You would need the URL for the webhook, and potentially other settings like the display name your app should use when posting to the channel:
public class SlackApiSettings
{
public string WebhookUrl { get; set; }
public string DisplayName { get; set; }
public bool ShouldNotify { get; set; }
}
You can bind this strongly typed settings object to your configuration in your Startup
class by using the Configure<T>()
extension method. For example:
public class Startup
{
public Startup(IConfiguration configuration) // inject the configuration into the constructor
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// bind the configuration to the SlackApi section
// i.e. SlackApi:WebhookUrl and SlackApi:DisplayName
services.Configure<SlackApiSettings>(Configuration.GetSection("SlackApi"));
}
public void Configure(IApplicationBuilder app)
{
app.UseMvc();
}
}
When you need to use the settings object in your app, you can inject an IOptions<SlackApiSettings>
into the constructor. For example, to inject the settings into an MVC controller:
public class TestController : Controller
{
private readonly SlackApiSettings _slackApiSettings;
public TestController(IOptions<SlackApiSettings> options)
{
_slackApiSettings = options.Value
}
public object Get()
{
//do something with _slackApiSettings, just return it as an example
return _slackApiSettings;
}
}
Behind the scenes, the ASP.NET Core configuration system creates a new instance of the SlackApiSettings
class, and attempts to bind each property to the configuration values contained in the IConfiguration
section. To retrieve the settings object, you access IOptions<T>.Value
, as shown in the constructor of TestController
.
Avoiding the IOptions
dependency
Some people (myself included) don't like that your classes are now dependent on IOptions
rather than just your settings object. You can avoid this dependency by binding the configuration object manually as described here, instead of using the Configure<T>
extension method. A simpler approach is to explicitly register the SlackApiSettings
object in the container, and delegate its resolution to the IOptions
object. For example:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Register the IOptions object
services.Configure<SlackApiSettings>(Configuration.GetSection("SlackApi"));
// Explicitly register the settings object by delegating to the IOptions object
services.AddSingleton(resolver =>
resolver.GetRequiredService<IOptions<SlackApiSettings>>().Value);
}
You can now inject the "raw" settings object into your classes, without taking a dependency on the Microsoft.Extensions.Options package. I find this preferable as the IOptions<T>
interface is largely just noise in this case.
public class TestController : Controller
{
private readonly SlackApiSettings _slackApiSettings;
// Directly inject the SlackApiSettings, no reference to IOptions needed!
public TestController(SlackApiSettings settings)
{
_slackApiSettings = settings;
}
public object Get()
{
//do something with _slackApiSettings, just return it as an example
return _slackApiSettings;
}
}
This generally works very nicely. I'm a big fan of strongly typed settings, and having first-class support for loading configuration from a wide range of locations is nice. But what happens if you mess up our configuration, maybe you have a typo in your JSON file, for example?
A more common scenario that I've run into is due to the need to store secrets outside of your source code repository. In particular, I've expected a secret configuration value to be available in a staging/production environment, but it wasn't set up correctly. Configuration errors like this are tricky, as they're only really reproducible in the environment in which they occur!
In the next section, I'll show how these sorts of errors can manifest in your application.
What happens if binding fails?
There's a number of different things that could go wrong when binding your strongly typed settings to configuration. In this section I'll show a few examples of errors that could occur by looking at the JSON output from the example TestController.Get
action above, which just prints out the values stored in the SlackApiSettings
object.
1. Typo in the section name
When you bind your configuration, you typically provide the name of the section to bind. If you think in terms of your appsettings.json file, the section is the key name for an object. "Logging"
and "SlackApi"
are sections in the following .json file:
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"SlackApi": {
"WebhookUrl": "http://example.com/test/url",
"DisplayName": "My fancy bot",
"ShouldNotify": true
}
}
In order to bind SlackApiSettings
to the "SlackApi"
section, you would call:
services.Configure<SlackApiSettings>(Configuration.GetSection("SlackApi"));
But what if there's a typo in the section name in your JSON file? Instead of SlackApi
, it says SlackApiSettings
for example. Hitting the TestController
API gives:
{"webhookUrl":null,"displayName":null,"shouldNotify":false}
All of the keys have their default
values, but there were no errors. The binding happened, it just bound to an empty configuration section. That's probably bad news, as your code is no doubt expecting webhookUrl
etc to be a valid Uri
!
2. Typo in a property name
In a similar vein, what happens if the section name is correct, but the property name is wrong. For example, what if WebhookUrl
appears as Url
in the configuration file? Looking at the output of the TestController
API:
{"webhookUrl":null,"displayName":"My fancy bot","shouldNotify":true}
As we have the correct section name, the DisplayName
and ShouldNotify
properties have have bound correctly to the configuration, but WebhookUrl
is null
due to the typo. Again, there's no indication from the binder that anything went wrong here.
3. Unbindable properties
The next issue is one that I see people running into now and again. If you use getter-only properties on your strongly typed settings object, they won't bind correctly. For example, if we update our settings object to use readonly properties:
public class SlackApiSettings
{
public string WebhookUrl { get; }
public string DisplayName { get; }
public bool ShouldNotify { get; }
}
and hit the TestController
endpoint again, we're back to default values, as the binder treats those properties as unbindable:
{"webhookUrl":null,"displayName":null,"shouldNotify":false}
4. Incompatible type values
The final error I want to mention is what happens if the binder tries to bind a property with an incompatible type. The configuration is all stored as strings, but the binder can convert to simple types. For example, it will bind "true"
or "FALSE"
to the bool ShouldNotify
property, but if you try to bind something else, "THE VALUE"
for example, you'll get an exception when the TestController
is loaded, and the binder attempts to create the IOptions<T>
object:
While not ideal, the fact the binder throws an exception that clearly indicates the problem is actually a good thing. Too many times I've been in a situation trying to figure out why some API call isn't working, only to discover that my connection string or base URL is empty, due to a binding error.
For configuration errors like this, it's preferable to fail as early as possible. Compile time is best, but app startup is a good second-best. The problem currently is that the binding doesn't occur until the IOptions<T>
object is requested from the DI container, i.e. when a request arrives for the TestController
. If you have a typo error, you don't even get an exception then - you'll have to wait till your code tries to do something invalid with your settings, and then it's often an infuriating NullReferenceException
!
To help with this problem, I use a slight re-purposing of the IStartupFilter
to create a simple validation step that runs when the app starts up, to ensure your settings are correct.
Creating a settings validation step with an IStartupFilter
The IStartupFilter
interface allows you to control the middleware pipeline indirectly, by adding services to your DI container. It's used by the ASP.NET Core framework to do things like add IIS middleware to the start of an app's middleware pipeline, or to add diagnostics middleware.
IStartupFilter
is a whole blog post on its own, so I won't go into detail here. Luckily, here's one I made earlier 🙂.
While IStartupFilter
s can be used to add middleware to the pipeline, they don't have to. Instead, they can simply be used to run some code when the app starts up, after service configuration has happened, but before the app starts handling requests. The DataProtectionStartupFilter
takes this approach for example, initialising the key ring just before the app starts handling requests.
This is the approach I suggest to solve the setting validation problem. First, create a simple interface that will be implemented by any settings that require validation:
public interface IValidatable
{
void Validate();
}
Next, create an IStartupFilter
to call Validate()
on all IValidatable
objects registered with the DI container:
public class SettingValidationStartupFilter : IStartupFilter
{
readonly IEnumerable<IValidatable> _validatableObjects;
public SettingValidationStartupFilter(IEnumerable<IValidatable> validatableObjects)
{
_validatableObjects = validatableObjects;
}
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
foreach (var validatableObject in _validatableObjects)
{
validatableObject.Validate();
}
//don't alter the configuration
return next;
}
}
This IStartupFilter
doesn't modify the middleware pipeline: it returns next
without modifying it. But if any IValidatable
s throw an exception, then the exception will bubble up, and prevent the app from starting.
You need to register the filter with the DI container, typically in ConfigureServices
:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IStartupFilter, SettingValidationStartupFilter>()
// other service configuration
}
Finally, you need to implement the IValidatable
interface on your settings that you want to validate at startup. This interface is intentionally very simple. The IStartupFilter
needs to execute synchronously, so you cant do anything extravagant here like calling HTTP endpoints or anything. The main idea is to catch issues in the binding process that you otherwise wouldn't catch till runtime, but you could obviously do some more in-depth testing.
To take the SlackApiSettings
example, we could implement IValidatable
to check that the URL and display name have been bound correctly. On top of that, we can check that the provided URL is actually a valid URL using the Uri
class:
public class SlackApiSettings : IValidatable
{
public string WebhookUrl { get; set; }
public string DisplayName { get; set; }
public bool ShouldNotify { get; set; }
public void Validate()
{
if (string.IsNullOrEmpty(WebhookUrl))
{
throw new Exception("SlackApiSettings.WebhookUrl must not be null or empty");
}
if (string.IsNullOrEmpty(DisplayName))
{
throw new Exception("SlackApiSettings.WebhookUrl must not be null or empty");
}
// throws a UriFormatException if not a valid URL
var uri = new Uri(WebhookUrl);
}
}
As an alternative to this imperative style, you could use DataAnnotationsAttribute
s instead, as suggested by Travis Illig in his excellent deep dive on configuration:
public class SlackApiSettings : IValidatable
{
[Required, Url]
public string WebhookUrl { get; set; }
[Required]
public string DisplayName { get; set; }
public bool ShouldNotify { get; set; }
public void Validate()
{
Validator.ValidateObject(this, new ValidationContext(this), validateAllProperties: true);
}
}
Whichever approach you use, the Validate()
method throws an exception if there is a problem with your configuration and binding.
The final step is to register the SlackApiSettings
as an IValidatable
object in ConfigureServices
. We can do this using the same pattern we did to remove the IOptions<>
dependency:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddTransient<IStartupFilter, SettingValidationStartupFilter>()
// Bind the configuration using IOptions
services.Configure<SlackApiSettings>(Configuration.GetSection("SlackApi"));
// Explicitly register the settings object so IOptions not required (optional)
services.AddSingleton(resolver =>
resolver.GetRequiredService<IOptions<SlackApiSettings>>().Value);
// Register as an IValidatable
services.AddSingleton<IValidatable>(resolver =>
resolver.GetRequiredService<IOptions<SlackApiSettings>>().Value);
}
That's all the configuration required, time to put it to the test.
Testing Configuration at app startup
We can test our validator by running any of the failure examples from earlier. For example, if we introduce a typo into the WebhookUrl
property, then when we start the app, and before we serve any requests, the app throws an Exception
:
Now if there's a configuration exception, you'll know about it as soon as possible, instead of only at runtime when you try and use the configuration. The app will never startup - if you're deploying to an environment with rolling deployments, for example Kubernetes, the deployment will never be healthy, which should ensure your previous healthy deployment remains active until you fix the configuration issue.
Using configuration validation in your own projects.
As you've seen, it doesn't take a lot of moving parts to get configuration validation working: you just need an IValidatable
interface and an IStartupFilter
, and then to wire everything up. Still, for people that want a drop in library to handle this, I've created a small NuGet package called NetEscapades.Configuration.Validation that contains the components, and a couple of helper methods for wiring up the DI container.
If you're using the package, you could rewrite the previous ConfigureServices
method as the following:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.UseConfigurationValidation();
services.ConfigureValidatableSetting<SlackApiSettings>(Configuration.GetSection("SlackApi"));
}
This will register the, IStartupFilter
, bind the SlackApiSettings
to your configuration, and also register the settings directly in the container (so you don't need to use IOptions<SlackApiSettings>
, and as an IValidatable
.
It's worth noting that validation only occurs once on app-startup. If you're using validation reloading with
IOptionsSnapshot<>
then this approach won't work for you.
Summary
The ASP.NET Core configuration system is very flexible and allows you to use strongly typed settings. However, partly due to this flexibility, it's possible to have configuration errors that only appear in certain environments. By default, these errors will only be discovered when your code attempts to use an invalid configuration value (if at all).
In this post, I showed how you could use an IStartupFilter
to validate your settings when your app starts up. This ensures you learn about configuration errors as soon as possible, instead of at runtime. The code in this post is available on GitHub, or as the NetEscapades.Configuration.Validation NuGet package.