Introduction
Accessing an application configuration using IConfiguration service provided by ASP.NET Core which brings up an aggregate of appsettings.json file along with several other providers, is simple and useful.
However, As the application grows in complexity, so does configuration in most cases: new configurations and sections add up to the size of the appsettings making individual values from the appsettings hard to access. For example, let’s assume we have an application whose appsettings json looks like this:
{
...
"Oidc": {
"Google": {
"ClientId": "27454jshd64dufmgngterh",
"ClientSecret": "39jfytittthsd83jgygtttktt7tyy8kthgh8"
},
"Facebook": {
"ClientId": "o375j6593jdgdb254en62rd",
"ClientSecret": "vhfyrfjhrrfdsow0273485djdgw722pr955ht"
},
"Okta": {
"ClientId": "a1b2c3d4e5f6g7h8i9j03fg",
"ClientSecret": "hfgfwornrtftfjnsfre693i34543u2gdjfbff"
}
},
"Smtp": {
"Server": "mail.abc.com",
"Port": "56",
"From": "me@abc.com",
"Username": "me@abc.com",
"Password": "Abcd@1234",
"IsSsl": false
},
"Jwt": {
"Audience": "thisisyouraudience",
"Issuer": "thisisyourissuers",
"SigningKey": "thiskeyisveryhardtobreak",
"IsValidateLifetime": true,
"IsValidateIssuer": true,
"IsValidateAudience": true
},
...
}
If we are to read the ClientId of the OidcProvider “Google”, we should access its value as below using the classic IConfiguration instance:
var googleClientId = Configuration["Oidc:Google:ClientId"].ToString();
As the configuration structure goes diverse with nested sections, its key representation grows lengthy and hard to maintain.
In a previous article, we tried to solve this by creating a strongly typed ConfigurationManager class which encapsulates this key access mechanism and also takes care of the typecasting requirements for non-string properties.
An even elegant solution for this is to “configure” these sections against their matching strongly typed classes and let ASP.NET Core handle the complexities for us.
Something like:
if i want to access the section Google from Oidc, I will configure the section against a known type and each time i request that section the container should provide me an instance of the type i configured before.
This notion is how Options pattern works in ASP.NET Core. The pattern helps in creating a “strongly-typed access” to sections in the configuration through a predefined mapping.
Back in our example, we can create strongly typed classes for the sections in the configuration JSON we saw before as below:
public class SmtpOptions
{
public const string SectionName = "Smtp";
public int Port { get; set; }
public string From { get; set; }
public string Server { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public bool IsSsl { get; set; }
}
We’ll configure the section name which will be bound to an instance of this class within the class itself, so that we can use this value while “configuring”, just to avoid using magic strings.
Just to remind that this class now represents this section of the configuration:
"Smtp": {
"Server": "mail.abc.com",
"Port": "56",
"From": "me@abc.com",
"Username": "me@abc.com",
"Password": "Abcd@1234",
"IsSsl": false
}
Ensure that:
- The property names and their types match the key names and their value types “exactly” in the configuration.
- The classes used must be non-abstract with parameterless constructors
- Only the public accessors (properties) are bound to configuration data but not fields
To create the “binding” we have two approaches:
- Use Configuration.Bind() to a new instance of the type and register it as a service
- Use the IOptions interface to let ASPNETCORE do the needful for us
In the first approach, we create a new instance of type SmtpOptions and then pass the instance to Configure.Bind() method against the section. Finally, we register this instance as a service of our choice to let the ASPNETCORE container maintain the instance for us.
# Startup.ConfigureServices() Method #
var smtp = new SmtpOptions();
Configuration.Bind(SmtpOptions.SectionName, smtp);
services.AddSingleton(smtp);
It can also be written as following using Get() instead of Bind(), which avoids the use of “new”:
# Startup.ConfigureServices() Method #
var smtp = Configuration.GetSection(SmtpOptions.SectionName)
.Get();
services.AddSingleton(smtp);
To use this instance in our logic, we can just inject the type through the constructor of the component where it is to be used.
public class MailController : ControllerBase
{
private readonly SmtpOptions smtp;
public MailController(SmtpOptions smtpOptions)
{
this.smtp = smtpOptions;
}
...
Although this approach works, we have two things here which aren’t that good:
- Using the “new” keyword – even in the world of dependency injection, why to explicitly “new” an instance?
- More code written – we’re literally instantiating, binding and then registering!
This paves way to the second approach which uses the IOptions set of interfaces. In this approach, we “configure” a type and its corresponding configuration section into the “options”, and access via the same.
For example, the above code can be rewritten as:
# Startup.ConfigureServices() Method #
services.Configure(
Configuration.GetSection(SmtpOptions.SectionName));
public class MailController : ControllerBase
{
private readonly SmtpOptions smtp;
public MailController(IOptions smtpOptions)
{
this.smtp = smtpOptions;
}
...
Observe the number of lines written – just one. And we’re also avoiding the “new” keyword. This makes the Options interface a “better” bet over the former. The IOptions interface is a part of the Microsoft.Extensions.Options namespace, which is implicitly available in ASPNETCORE core package.
Types of Options
There are three interfaces provided by this namespace as a part of configuring TOptions instances.
- IOptions
- IOptionsSnapshot and
- IOptionsMonitor
We can use IOptionsSnapshot instead of IOptions in the above example to inject in a Controller as below:
public class MailController : ControllerBase
{
private readonly SmtpOptions smtp;
public MailController(IOptionsSnapshot smtpOptions)
{
this.smtp = smtpOptions.Value;
}
...
IOptionsMonitor is another way we can access the configured options, where the “CurrentValue” of the options is returned each time the service is requested.
public class MailController : ControllerBase
{
private readonly SmtpOptions smtp;
public MailController(IOptionsMonitor smtpOptions)
{
this.smtp = smtpOptions.CurrentValue;
}
...
Differences between IOptions, IOptionsSnapshot and IOptionsMonitor
IOptionsSnapshot
- Enables us to load configuration changes even after app has started
- It is registered as a ScopedService and can’t be injected into Singleton services
- Since it’s a ScopedService, its better suited for scenarios where the configuration needs to be re-read for every request
- Supports “named” options
IOptions
- It is registered as a SingletonService and can be injected into any service
- Since it’s a singleton, configuration changes can’t be re-read once instansiated
- Doesn’t support “named” options
IOptionsMonitor
- It is registered as a SingletonService similar to IOptions and can be injected into any service
- It can also be used to read reloaded configurations as opposed to IOptions, hence it gives us the best of both worlds – IOptionsSnapshot and IOptions
- Also supports “named” options
When discussing about IOptionsSnapshot and IOptionsMonitor, we see they support something called “named” options. Now what is a “named” option?
What are Named Options?
Until this section, we have been configuring and using options monotonously in a single section – to – single model approach. However, consider the section “Oidc” which has three subsections as below:
...
"Oidc": {
"Google": {
"ClientId": "27454jshd64dufmgngterh",
"ClientSecret": "39jfytittthsd83jgygtttktt7tyy8kthgh8"
},
"Facebook": {
"ClientId": "o375j6593jdgdb254en62rd",
"ClientSecret": "vhfyrfjhrrfdsow0273485djdgw722pr955ht"
},
"Okta": {
"ClientId": "a1b2c3d4e5f6g7h8i9j03fg",
"ClientSecret": "hfgfwornrtftfjnsfre693i34543u2gdjfbff"
}
}
...
All the three sections are formed in the same manner, but they belong to different “scenarios”. We can’t just clone three different classes of the same structure just to translate this block.
In such cases, we use the “named” options overload which is supported by IOptionsSnapshot and IOptionsMonitor options.
Let’s create a single template that resembles all three subsections perfectly.
public class OidcProviders
{
public const string Google = "Google";
public const string Facebook = "Facebook";
public const string Okta = "Okta";
}
public class OidcOptions
{
public const string SectionName = "Oidc";
public string ClientId { get; set; }
public string ClientSecret { get; set; }
}
The OidcOptions class mimics each subsection of the “Oidc” section, while OidcProviders represent their subsection key names. We shall now configure these subsections individually as below:
#Oidc:Google#
services.Configure(OidcProviders.Google, Configuration.GetSection($"{OidcOptions.SectionName}:{OidcProviders.Google}"));
#Oidc:Facebook#
services.Configure(OidcProviders.Facebook, Configuration.GetSection($"{OidcOptions.SectionName}:{OidcProviders.Facebook}"));
#Oidc:Okta#
services.Configure(OidcProviders.Okta, Configuration.GetSection($"{OidcOptions.SectionName}:{OidcProviders.Okta}"));
To access these options at a required component, we can request an instance tagged to a specific “name”, which in this case is the KeyNames declared in the OidcProviders class.
public class OidcOptionsController : ControllerBase
{
private readonly OidcOptions google, facebook, okta;
public OidcOptionsController(
IOptionsSnapshot oidcOptions)
{
google = oidcOptions.Get(OidcProviders.Google);
facebook = oidcOptions.Get(OidcProviders.Facebook);
okta = oidcOptions.Get(OidcProviders.Okta);
}
...
}
Observe that we’re requesting objects based on the KeyName constants. Let’s see how these objects turn out to be by adding a simple GET endpoint.
[HttpGet, Route("oidc")]
public Dictionary<string, oidcoptions=""> GetOidcOptions()
{
return new Dictionary<string, oidcoptions="">()
{
{ OidcProviders.Google, google },
{ OidcProviders.Facebook, facebook },
{ OidcProviders.Okta, okta }
};
}
The output:
{"Google":{"clientId":"27454jshd64dufmgngterh","clientSecret":"39jfytittthsd83jgygtttktt7tyy8kthgh8"},"Facebook":{"clientId":"o375j6593jdgdb254en62rd","clientSecret":"vhfyrfjhrrfdsow0273485djdgw722pr955ht"},"Okta":{"clientId":"a1b2c3d4e5f6g7h8i9j03fg","clientSecret":"hfgfwornrtftfjnsfre693i34543u2gdjfbff"}}
“Each time an IOptionsSnapshot or IOptionsMonitor is called without a “name” passed, we are actually invoking the “named” overload of the Get() method with the default KeyName, which is String.Empty or “”.”
IConfigureNamedOptions for customizing Options
In the example of Oidc, let’s assume that the values ClientId and ClientSecret are actually encrypted and placed in the appsettings, and before using them they must be decrypted and stored in the objects.
This requires us to work a little bit more on the OidcOptions instances before accessing them. Also, this requires an external service say IDecryptService for decrypting the values.
In such scenarios, we make use of the IConfigureNamedOptions interface, which is internally called whenever the IOptionsSnapshot or IOptionsMonitor – the “named” options are called. The implementation is done as below:
public class ConfigureOidcOptions : IConfigureNamedOptions
{
private readonly IDecryptService decrypt;
public ConfigureOidcOptions(IDecryptService decrypt)
{
this.decrypt = decrypt;
}
// implementation which is always called
public void Configure(string name, OidcOptions options)
{
options.ClientId = this.decrypt.Decrypt(options.ClientId);
options.ClientSecret = this.decrypt.Decrypt(options.ClientSecret);
}
// implementation just for the interface
public void Configure(OidcOptions options)
{
Configure(Options.DefaultName, options);
}
}
And we register this implementation of the IConfigureNamedOptions, as a SingletonService resolved for the interface IConfigureOptions. This works because IConfigureNamedOptions is a subtype of IConfigureOptions.
#Oidc:Google#
services.Configure<OidcOptions>(OidcProviders.Google, Configuration.GetSection($"{OidcOptions.SectionName}:{OidcProviders.Google}"));
#Oidc:Facebook#
services.Configure<OidcOptions>(OidcProviders.Facebook, Configuration.GetSection($"{OidcOptions.SectionName}:{OidcProviders.Facebook}"));
#Oidc:Okta#
services.Configure<OidcOptions>(OidcProviders.Okta, Configuration.GetSection($"{OidcOptions.SectionName}:{OidcProviders.Okta}"));
services.AddSingleton<IConfigureOptions<OidcOptions>, ConfigureOidcOptions>();
And when we run this and check the value of Oidc resolved using our GET endpoint, the output is as follows:
{"Google":{"clientId":"Mjc0NTRqc2hkNjRkdWZtZ25ndGVyaA==","clientSecret":"MzlqZnl0aXR0dGhzZDgzamd5Z3R0dGt0dDd0eXk4a3RoZ2g4"},"Facebook":{"clientId":"bzM3NWo2NTkzamRnZGIyNTRlbjYycmQ=","clientSecret":"dmhmeXJmamhycmZkc293MDI3MzQ4NWRqZGd3NzIycHI5NTVodA=="},"Okta":{"clientId":"YTFiMmMzZDRlNWY2ZzdoOGk5ajAzZmc=","clientSecret":"aGZnZndvcm5ydGZ0Zmpuc2ZyZTY5M2kzNDU0M3UyZ2RqZmJmZg=="}}
Conclusion and… boilerplate 🥳
In this way, we can make use of the Options pattern in the ASP.NET Core to leverage the benefits of DI and create a robust and strictly typed configuration system.
The complete example project used in this article is available under https://github.com/referbruv/options-pattern-example