As software developers, we sure do love us some options, especially when it allows us to change application behavior without reimplementing or compiling our apps. The developer gods have carved configuration into our collective dev DNA. Whether you’re new or old to ASP.NET, you’ll likely want to take advantage of settings derived from json
files. In this post, we’ll explore the necessary steps required to read configuration settings from disk and the options (pun intended) to working with those values.
ASP.NET Configuration History
For those grizzled veterans of the ASP.NET ecosystem, you might remember the web.config
file. While not wholly abandoned, it plays a less pivotal role in ASP.NET Core applications. In the context of ASP.NET “classic”, the web.config
is an XML based file meant to configure the host environment of Internet Information Services (IIS). In this file, we can place application settings, load additional web modules, register handlers, and much more. To say this file is intimidating is an understatement.
Another limitation of the previous web.config
approach was file changes would force applications to reboot. Changes could be as simple as adding a new application setting or as complex as adding new modules to the request pipeline. ASP.NET applications had to reload to ensure logical consistency. Developers could work around this limitation by moving all settings outside the traditional ConfigurationManager
access methods and developing custom setting solutions. Frankly, as time progressed, developers saw reboots as more of a feature than a hindrance, using it to reset applications that may have fallen into a failure state.
Present ASP.NET Core
ASP.NET Core saw the main issues around configuration and attempted to improve the story for developers around three central points:
- Support for a theoretically unlimited set of configuration providers in addition to XML.
- Replacing the
ConfigurationManager
singleton with dependency injection (DI) friendly approach. - Hot changes to settings can occur immediately in the code accessing values.
These changes reflect a more modern take on settings and are friendlier to cloud-native web applications. Application settings can come from several locations, and providing options and extensibility makes it easier for developers to avoid custom solutions.
With .NET adopting async/await
and asynchronous programming, singletons usage can lead to deadlocks and performance overhead. Backing DI into configuration gives developers more options to consume setting dependencies and decouple these bits of data from any single source. It also makes testing configuration settings without accessing ConfigurationManager
or the web.config
.
Cold starts are the enemy of users everywhere, creating a frustrating experience. The ability to make changes without rebooting ensures a more consistent runtime experience.
Working With Code
So, we’ve talked about the history and current state of configuration, but let’s jump into actually using settings in an ASP.NET Core application.
The first step is to create a data class that will read settings from our configuration providers. ASP.NET Core ships with multiple out of the box, but the most commonly used is the JSON
configuration provider. The class needs to contain the same structure as our eventual JSON section.
public class HelloWorldOptions
{
public string Text { get; set; }
}
The next part is to tell ASP.NET Core can find the data for the HelloWorldOptions
class. We can do this using the BindConfiguration
method.
public void ConfigureServices(IServiceCollection services)
{
services
.AddOptions<HelloWorldOptions>()
.BindConfiguration("HelloWorld");
}
The string HelloWorld
indicates the section in our appsettings.json
file.
{
"HelloWorld" : {
"Text": "Hello, Khalid!"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
Great, now we’re ready to consume our configuration settings. Here’s where it gets a little confusing. We have three interfaces to choose from:
IOptions<T>
IOptionsSnapshot<T>
IOptionsMonitor<T>
Me trying to pick the correct IOptions wrapper in #aspnetcore… I should have studied my Roman numerals. Right @maartenballiauw? @dotnet #aspnet 🐅🐯 pic.twitter.com/UsV7sUZy6Q
— Khalid 🎅🎄 (@buhakmeh) December 3, 2020
Each of these interfaces wraps our configuration data and gives us a slightly different service lifetime behavior.
IOptions<T>
is registered as a singleton, and thus all values are retrieved once and stored in the ASP.NET Core application’s memory. This approach cannot read any updated configuration after the application has started. Registration as a singleton means ASP.NET can inject the interface into any dependency without fear of capturing it or causing memory leak issues. This version is likely what most folks will use.
IOptionsSnapshot<T>
instances have a Scoped
lifetime. ASP.NET Core will recompute once per HTTP request. Caching the instance per request ensures consistency until the user receives a response. The snapshot approach is useful for folks who want to change behavior on the fly but still need ongoing requests to flush through the current pipeline. This version is helpful for use with feature flags or toggling behavior without reloading the application.
Finally, IOptionsMonitor<T>
is very similar to IOptionsSnapshot<T>
but has a lifetime of Singleton
. The monitor approach is useful for critical data changes should be reflected immediately, or when IOptionsMonitor<T>
is located within another Singleton
. Long-lived background services might want to use the IOptionsMonitor
instance to continue receiving settings changes but not pay the expensive object creation cost.
Great! Now that we know what strengths each interface has, we can consume our settings. In our Configure
method found in Startup
, let’s add a new GET
endpoint.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
var options =
context
.RequestServices
.GetRequiredService<IOptionsSnapshot<HelloWorldOptions>>()
.Value;
await context.Response.WriteAsync(options.Text);
});
});
}
Note that in our endpoint, we’re using the IOptionsSnapshot
. Utilizing the instance will allow us to update our configuration without the need to reboot our application. Starting the application, we should see our setting value our client.
Hello, Khalid!
Changing our configuration will change the result of our request.
{
"HelloWorld" : {
"Text": "Hello, World!"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
Reloading the page we now see.
Hello, World!
Wow! That just worked, and we didn’t have to pay the start-up costs for a tiny change in our settings.
Conclusion
It might be confusing at first using the configuration in ASP.NET Core. The Microsoft documentation has a thorough explanation of the option interfaces. In most cases, folks should use IOptions<T>
as it is likely the most performant due to its lifetime registration of Singleton
and lack of reloading support. That said, if we want the ability to hot-reload settings, IOptionsSnapshot
is best if we wish to have request consistency. Finally, if your app is heavily relying on Singleton
lifetimes and still need hot-reload settings, consider IOptionsMonitor
.
I hope you found this post enlightening, and please leave a comment as to which one of these interfaces your primarily use in your applications.