Photo by Manik Roy
I want my configuration objects to be plain pocos. I don't want to depend on the IOptions<ExampleOptions> pattern in my code, I want to inject the poco (without the IOptions wrapper).
I also don't care about/want any reloading of the configuration during runtime. What I usually do is registering my options as singletons.
One way of doing this, while still using the options helper methods when registering your options is something like this:
services.AddOptions<BlogExampleOptions>()
.Bind(Configuration.GetSection("BlogExample"));
services.AddSingleton<BlogExampleOptions>(x => {
return x.GetRequiredValue<IOptions<BlogExampleOptions>>.Value;
});
The above code registers a BlogExampleOptions singleton (it also registers a IOptions<BlogExampleOptions> instance). But yeah, I don't know, it doesn't feel quite right you know?
Let's see if we can make it...cleaner? Better?
Dealing with configuration
"POCO Options"
I have the following options:
public class BlogExampleOptions
{
public string Name { get; set; }
public bool Enabled { get; set; }
public DateTimeOffset SomeDate { get; set; }
}
I then register it as a singleton:
var exampleOptions = new BlogExampleOptions();
var configurationSection = _configuration.GetSection("BlogExample");
_configurationSection.Bind(exampleOptions);
services.AddSingleton<BlogExampleOptions>(exampleOptions);
I can then inject the options like this:
public class SomeClass
{
public SomeClass(BlogExampleOptions options)
{
// do something with options.
}
}
The above works perfectly fine, but the setup of the BlogExampleOptions is quite cumbersome and error-prone. What if I've forgotten to include the configuration for BlogExampleOptions for example?
I've created some extension methods on IConfiguration and IServiceCollection to make the developer experience better.
So this...
var exampleOptions = new BlogExampleOptions();
var configurationSection = _configuration.GetSection("BlogExample");
_configurationSection.Bind(exampleOptions);
services.AddSingleton<BlogExampleOptions>(exampleOptions);
...becomes this
services.AddPocoOptions<BlogExampleOptions>("BlogExample", _configuration);
If you need the options straight away you can use the out
overload like this:
services.AddPocoOptions<BlogExampleOptions>("BlogExample", _configuration, out var options);
AddPocoOptions
is just a thin wrapper over another extension method I've created; GetRequiredOptions
(more about that method below).
"Simple values"
Sometimes I just want a simple value from the configuration without mapping it to some options object.
Example:
var name = _configuration.GetValue<string>("BlogExample:Name");
if(string.isNullOrWhiteSpace(name))
{
throw new Exception("Missing name in configuration");
}
The problem with the code above is that the value can be null. I don't like null.
So let's use my GetRequiredValue
method instead.
var name = _configuration.GetRequiredValue<string>("BlogExample:Name");
If I've forgotten to add that configuration, the following exception will be thrown:
"'BlogExample:Name' had no value, have you forgot to add it to the Configuration?"
Just give me the code
IConfigurationExtensions
public static class ConfigurationExtensions
{
public static T GetRequiredValue<T>(
this IConfiguration configuration,
string key)
{
var value = configuration.GetValue(typeof(T?), key);
if (value == null)
{
throw MissingRequiredKeyException(key);
}
return (T)value;
}
public static T GetRequiredOptions<T>(
this IConfiguration configuration,
string key
) where T : new()
{
var configurationSection = configuration.GetRequiredSection(key);
var data = new T();
configurationSection.Bind(data);
return data;
}
public static IEnumerable<T> GetRequiredValues<T>(this IConfiguration configuration, string key)
{
var configurationSection = configuration.GetRequiredSection(key);
var target = new List<T>();
configurationSection.Bind(target);
return target;
}
// Inspired by GetRequiredSection in dotnet 6
// https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.configuration.configurationextensions.getrequiredsection?view=dotnet-plat-ext-6.0&WT.mc_id=DT-MVP-5004074
// Replace with built in method when upgrading this to dotnet 6
private static IConfigurationSection GetRequiredSection(this IConfiguration configuration, string key)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
var section = configuration.GetSection(key);
if (section.Exists())
{
return section;
}
throw new Exception($"Section '{key}' not found in configuration.");
}
private static bool Exists(this IConfigurationSection? section)
{
if (section == null)
{
return false;
}
return section.Value != null || section.GetChildren().Any();
}
private static Exception MissingRequiredKeyException(string key) =>
throw new Exception($"'{key}' had no value, have you forgot to add it to the Configuration?");
IServiceCollectionExtensions
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddPocoOptions<T>(
this IServiceCollection services,
string key,
IConfiguration configuration) where T : class, new()
{
var options = configuration.GetRequiredOptions<T>(key);
services.AddSingleton<T>(options);
return services;
}
public static IServiceCollection AddPocoOptions<T>(
this IServiceCollection services,
string key,
IConfiguration configuration, out T options) where T : class, new()
{
options = configuration.GetRequiredOptions<T>(key);
services.AddSingleton<T>(options);
return services;
}
}