In this episode we take a look at configuration in ASP.NET Core, the possible sources, how to read from them, the options pattern, wrapping up with development time secrets.
For the walk-through you can check the next video, but if you prefer a quick read, skip to the written synthesis.
The playlist for the whole series is here.
Intro
In ASP.NET Core the way configuration is managed really changed when comparing with ASP.NET, where we had the web.config
file (and others with the same format). In this post, we'll take a look at this new configuration, and see how much more powerful it is.
For much more in depth information about this topic, be sure to check out the docs over here.
Configuration providers
One of the great things introduced with ASP.NET Core configuration, is the ability to have multiple configuration providers, but being able to then use those configurations in the code in a consistent manner, regardless of their source.
So for instance, we can have configurations done through environment variables, command-line arguments and a JSON file, and then use them as if they all came from the same place.
(Copied from the docs) we currently have the following providers available:
Provider | Provides configuration from |
---|---|
Azure Key Vault Configuration Provider | Azure Key Vault |
Command-line Configuration Provider | Command-line parameters |
Custom configuration provider | Custom source |
Environment Variables Configuration Provider | Environment variables |
File Configuration Provider | Files (INI, JSON, XML) |
Key-per-file Configuration Provider | Directory files |
Memory Configuration Provider | In-memory collections |
User secrets (Secret Manager) | File in the user profile directory |
As you can see in the table above, we can also create our own provider, to access a different configuration format or location, but that's out of scope for this post, we'll just play a bit with some of the existing providers.
To provide a glimpse of what we can do with ASP.NET Core's configuration infrastructure, in this post we'll play a little bit with the command-line, file and user secrets providers.
Configuring providers
The first thing we need to do is configure the configuration providers we want to use (or as we'll see, not really 😛).
To configure the providers, we go into the Program
class and where we're creating the IWebHostBuilder
, we can call the ConfigureAppConfiguration
method to setup the providers we want.
As we saw in previous episodes, the WebHost
CreateDefaultBuilder
method already comes with a lot of things setup out of the box, and configuration providers are one of them. If we take a look at the method's source, we can see what's already there.
//...
.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment;
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
if (env.IsDevelopment())
{
var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
if (appAssembly != null)
{
config.AddUserSecrets(appAssembly, optional: true);
}
}
config.AddEnvironmentVariables();
if (args != null)
{
config.AddCommandLine(args);
}
})
//...
(complete source on GitHub)
By looking at the source, we can see that 5 providers are configured:
- 2 for JSON files, the first one named
appsettings.json
and the other the same plus the name of the current environment - a user secrets provider, only when in a development environment
- environment variables provider
- command line arguments provider
An important thing to point out is that the order in which the providers are configured is relevant. An added provider overrides any previous one's configuration with the same key. For instance, if we set a value in appsettings.json
but then we want a different one in development mode, we simply put a configuration with the same key in appsettings.development.json
. Likewise for other providers.
Given these providers are already setup, for this post's objective, no need to configure anything else.
Creating some configurations
Before we see how to use the configurations in the code, it's probably a good idea to start by creating some 😀
Let's start by creating a couple of JSON files in the root of the project - appsettings.json
and appsettings.development.json
- and add the following contents to them.
appsettings.json
{
"SomeRoot": {
"SomeSubRoot": {
"SomeKey": 12345,
"AnotherKey": "QWERTY"
}
}
}
appsettings.development.json
{
"SomeRoot": {
"SomeSubRoot": {
"SomeKey": 67890
}
}
}
To pass in some configurations through the command line we can do dotnet run -- --SomeRoot:SomeSubRoot:CmdLineKey 13579
. The colon is used to define a hierarchy, so the equivalent in JSON would be:
{
"SomeRoot": {
"SomeSubRoot": {
"CmdLineKey": 13579
}
}
}
If you're using an IDE to run the application, there's probably a menu somewhere that lets you pass in command line arguments.
- In Visual Studio 2017 you would -> right click project -> "Properties" -> "Debug" -> "Application Arguments" (note that this doesn't work when using IIS Express)
- In JetBrains Rider, which is what I'm using, you go into the run configurations, and add the arguments into the "Program arguments" input
Now that we have some configurations to access, let's get to it.
Accessing configuration
Let's start with the simplest (but not great) way to access the configuration, using IConfiguration
.
In our Startup
class, or even in our controllers or services, we can get an IConfiguration
injected, and use it to access the configurations. Then we can, for instance, do the following to get some values:
config.GetValue<int>("SomeRoot:SomeSubRoot:SomeKey"); //returns 67890
config.GetValue<int>("SomeRoot:SomeSubRoot:CmdLineKey"); //returns 13579
//or
var section = config.GetSection("SomeRoot:SomeSubRoot");
section.GetValue<string>("AnotherKey"); //returns "QWERTY"
Even though this works, and for really simple stuff may be enough, it's not great, and spreading strings like "SomeRoot:SomeSubRoot:SomeKey"
around the application isn't very nice (even if we put it in constants).
What would be much nicer than this way of accessing our configurations, would be to have classes that represent them, providing a much cleaner and type safe way to access those values. To solve this problem, enter the options pattern.
Options pattern
The options pattern introduced in ASP.NET Core allows us to easily bind the configurations to POCOs. To map what we setup previously, we create a couple of classes:
SomeRootConfiguration.cs
public class SomeRootConfiguration
{
public SomeSubRootConfiguration SomeSubRoot { get; set; }
}
SomeSubRootConfiguration.cs
public class SomeSubRootConfiguration
{
public int SomeKey { get; set; }
public string AnotherKey { get; set; }
public int CmdLineKey { get; set; }
}
Then to bind the configurations to these classes, we can add the following line to Startup
's ConfigureServices
method:
Startup.cs
public class Startup
{
private readonly IConfiguration _config;
public Startup(IConfiguration config)
{
_config = config;
}
public IServiceProvider ConfigureServices(IServiceCollection services)
{
//...
services.Configure<SomeRootConfiguration>(_config.GetSection("SomeRoot"));
//...
}
//...
}
Then to use it, we can go into the GroupsController
and add IOptions<SomeRootConfiguration>
to use our nicely structured configuration class. Alternatively, we can inject IOptionsSnapshot<SomeRootConfiguration>
, which allows the application to load configurations changed at runtime.
Avoiding IOptions injection
Even though the options pattern is pretty great improvement to what we saw before it, it can be further improved.
Unless for the reloading capabilities, injecting IOptions
isn't really useful, and adds an extra dependency that's really not required. When injecting into controllers, this extra using
isn't a big deal, as we're already in ASP.NET Core MVC land. But what about injecting it into other services/classes? Forcing a dependency just to inject configurations in other libraries, that may be used in a non ASP.NET Core application (e.g. a console application, a web application developed using a different web framework) is a harder pill to swallow.
Luckily, it's an easy problem to solve. For a more detailed discussion on this you can check this excellent article, but I'll quickly walk through a solution here.
Instead of using the services.Configure
method, we can bind the configuration directly to the class, and then inject it as a singleton. We can do that as follows:
Startup.cs
public IServiceProvider ConfigureServices(IServiceCollection services)
{
//...
var someRootConfiguration = new SomeRootConfiguration();
_config.GetSection("SomeRoot").Bind(someRootConfiguration);
services.AddSingleton(someRootConfiguration);
//...
}
This solves our problem and now, in the controller, we can simply inject a SomeRootConfiguration
, no IOptions
needed.
To avoid repeating this code for all types of configurations we want to add, we can create an extension method to handle this logic.
ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
//...
public static TConfig ConfigurePOCO<TConfig>(this IServiceCollection services, IConfiguration configuration)
where TConfig : class, new()
{
if (services == null) throw new ArgumentNullException(nameof(services));
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
var config = new TConfig();
configuration.Bind(config);
services.AddSingleton(config);
return config;
}
}
And we can replace the previous configuration registration with:
Startup.cs
public IServiceProvider ConfigureServices(IServiceCollection services)
{
//...
services.ConfigurePOCO<SomeRootConfiguration>(_config.GetSection("SomeRoot"));
//...
}
Which now looks pretty much like the usual configuration registration, but without the IOptions
burden.
User secrets
Another interesting configuration provider we should take a look at is the user secrets provider.
The user secrets provider is used during development to keep secrets from ending up in source control, like API keys, connection strings and things like that. In production you would probably use other means of providing those kinds of configurations, like environment variables or services to keep secrets (like Azure Key Vault).
User secrets are not encrypted, they're simply stored in a different path to avoid adding them to source control by accident. For a lot more info on user secrets, head on to the docs.
Before adding secrets to the application, we need to add an identifier to it, so when starting the application can fetch from the secrets store the correct configurations. To do this, head into the application's csproj
and add a GUID inside an UserSecretsId
element below the TargetFramework
.
In this case, we go into CodingMilitia.PlayBall.GroupManagement.Web.csproj
and do:
<!-- ... -->
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<UserSecretsId>A1CD619E-A374-4208-888B-42F3DC489F14</UserSecretsId>
</PropertyGroup>
<!-- ... -->
Now we can add some secrets by doing dotnet user-secrets set "DemoSecrets:SomeKey" "02468"
.
To access the configured secrets, we do exactly the same as in the other types of providers, so I just created another POCO and configured it like we saw in the previous section.
DemoSecretsConfiguration.cs
public class DemoSecretsConfiguration
{
public int SomeKey { get; set; }
}
Startup.cs
public IServiceProvider ConfigureServices(IServiceCollection services)
{
//...
services.ConfigurePOCO<DemoSecretsConfiguration>(_config.GetSection("DemoSecrets"));
//...
}
Pretty neat I would say 😀
Outro
That's a wrap for this intro to ASP.NET Core configuration. As usual, there's a lot more to explore, but I think this covers the bases. Be sure to head on to the docs for a lot more information (the documentation for ASP.NET Core is pretty great in general).
The source code for this post is here.
Please send any feedback you have, so the next posts/videos can be better and even adjusted to more interesting topics.
Thanks for stopping by, cyaz!
Linked articles
Strongly typed configuration in ASP.NET Core without IOptions<T>
Top comments (0)