Dependency Injection of AppSettings in ASP.NET Core

December 30, 2021 in Dependency Injection Testing
Read Time: 5 minutes
Dependency Injection of AppSettings in ASP.NET Core

When talking about the Dependency Inversion Principle, the D in SOLID, we have a pretty good idea of what dependencies we’re trying to abstract away. OrderRepository or SendGridEmailClient are easy examples of implementations we should abstract away. It also overlaps well with the Single Responsibility Principle, since data access for orders and sending emails are separated from the business logic where they are used. The C# implementation of this would involve making these classes implement the appropriate interfaces, e.g. IOrderRepository and IEmailSender.

With those examples, dependencies seem easy to identify: service from another module in your code, data access libraries like repositories, external or 3rd party services like HTTP clients, or something sending email. One dependency I hadn’t given much thought to is the settings the application relies on.

In .NET Framework, these are stored in the web.config file and are accessed with ConfigurationManager. In .NET Core, these have moved to (the much more flexible) appsettings.json files and are accessed using IConfiguration.

So What’s the Problem?

It’s not so much a problem as much as it is an opportunity for improvement. When testing, it’s easy enough to add an app.config or appsettings.json file specific to the test project. That has worked for me fine for years. One issue I’ve run into is the difficulty of changing settings for a specific test. Yes, you could change the settings before the test run and reset it after, but consider this:

 1public class OrdersController
 2{
 3    public bool Delete(long id)
 4    {
 5        var order = _db.Orders.Find(id);
 6        if (bool.Convert(ConfigurationManager.AppSettings["IsSoftDelete"]))
 7        {
 8            order.IsDeleted = true;
 9        }
10        else
11        {
12            _db.Orders.Remove(order);
13        }
14        _db.SaveChanges();
15    }
16}

Line 6 is our decision point to decide whether we do a soft delete or a hard delete of an order. Testing this isn’t impossible. You could set ConfigurationManager.AppSettings["IsSoftDelete"] = 'True/False' before the test and rest it after. The casting of the string to a boolean is a bit awkward here. But, for me, it’s the possible repetition of the magic string IsSoftDelete. Both of these things can be error-prone.

The Alternative

 1public class OrdersController
 2{
 3    private readonly Settings _settings;
 4
 5    public OrdersController(Settings settings) => _settings = settings;
 6
 7    public bool Delete(long id)
 8    {
 9        var order = _db.Orders.Find(id);
10        if (_settings.IsSoftDelete)
11        {
12            order.IsDeleted = true;
13        }
14        else
15        {
16            _db.Orders.Remove(order);
17        }
18        _db.SaveChanges();
19    }
20}

Notice we are injecting the Settings class in through the constructor. This is a class that could represent the full structure of the application settings. This is better for several reasons.

  • A concrete POCO (plain old C# object) is refactoring friendly. You can rename fields and be sure they are consistent everywhere in the application.
  • Compiler assistance against typos.
  • Intellisense/autocomplete in any decent IDE. This can help with the auto-discovery of other settings.
  • Elimination of magic strings

Testing becomes easier and more explicit. We can specifically inject a test instance of the settings class with the values you care about. Because Settings is just a POCO, it’s trivial to new up and pass to the class you’re testing.

Of course, we still have to map the Settings class to the file settings, through ConfigurationManager somewhere. This would usually be done in some bootstrap code somewhere and registered using your IoC (Inversion of Control) container.

The Implementation

.NET Framework

The mapping code:

var settings = new Settings
{
    IsSoftDelete = bool.Convert(ConfigurationManager.AppSettings["IsSoftDelete"]),
    ApiKey = ConfigurationManager.AppSettings["MyAPIKey"],
    ...
};
kernel.Bind(settings); // using Ninject to bind to this specific instance of the class

In this example, we’re essentially registering this as a singleton. For most of my implementations, settings don’t change after the application starts up. Even though the web.config file can be changed, it triggers a reload of the running application anyway.

If you’re running a console application or a Windows Service, your registration may look different.

.NET Core

The appSettings.json file in .NET Core is much more flexible than AppSettings in web.config. You can have more complex objects, with sub-classes, and even collections. The process is similar:

  • Define a POCO that maps to your settings file.
  • Map the settings from the file to the class
  • Register these into your IoC container.

This is almost trivial in .NET Core using the Options Pattern.

Consider these extension methods:

public static IServiceCollection ConfigureSettings(this IServiceCollection services, IConfiguration configuration) where T : class, new()
{
    services.Configure<Settings>(configuration); // configure the whole settings file
    // OR
    services.Configure<Settings>(configuration.GetSection("Settings")); // bind to a specific section of the settings file.
    return services;
}

Configuring the settings like this will register the class using the IOptions interface. Settings can be accessed as follows:

public class MyService
{
    private readonly Settings _settings;
    
    public MyService(IOptions<Settings> options)
    {
        _settings = options.Value;
    }
}

This works great, but I prefer adding one additional step:

 services.AddSingleton(x => x.GetService<IOptions<Settings>>().Value);

This way I skip having to tie myself to the IOptions interface (which is just another abstraction I don’t need). Using this becomes exactly the same as the .NET Framework example:

public class MyService
{
    private readonly Settings _settings;
    
    public MyService(Settings settings)
    {
        _settings = settings;
    }
}

Another alternative to registering is to skip the Options Pattern entirely:

var settings = configuration.Get<Settings>(); // to bind the whole file
// OR
var settings = configuration.GetSection("Settings").Get<Settings>(); // to bind to a section of the file

services.AddSingleton(settings); // register this instance.

I find this a more straightforward implementation if I don’t need the added benefits of IOptions.

Reusability

public static IServiceCollection BindSettings<T>(this IServiceCollection services, IConfiguration configuration, string section = null) where T : class, new()
{
    T settings;
    if (section == null)
    {
        settings = configuration.Get<T>();
    }
    else
    {
        settings = configuration.GetSection(section).Get<T>();
    }
    services.AddSingleton(settings);
    return services;
}

This extension method will allow easy registration of settings.

References

Comments

comments powered by Disqus