Make .NET configuration system flexible again

3 minute read | July 23, 2017

Hi everyone! Today I want to talk about brand-new .NET Core configuration system.

As all we know, new versions of Microsoft's frameworks were written in the modular model. (ASP.NET isn’t one dll anymore, it’s a set of packages which can be turned on by demand).

A few weeks ago, I decided to wrote a simple api-consumer with using of ASP.NET Core.

App architecture

So, I wanted to implement a simple app from REST-endpoint and background service for scraping data from vk.com api. I planned that in the some interval of time background service makes requests to api and fetches fresh data. So, I decided to make a layer architecture. After this decision, some interesting cases related with brand-new .NET core configuration system were found. I solved them by creating wrapper around this system.
Show me the code

Same setting for two or three projects

We all living in the world, where microservices is not a just another one hipster word, but also real tendency in the world of big systems’ architecture. But I think that even in this world there is a place for settings which can be same for the multiple projects. And my web-scraper is about this situation.

Entity Framework Core and Migrations

Nowadays, DI is the importan part of .NET stack. If you want to configure DbContext and open official documentation, you will find some examples.

public class BloggingContext : DbContext
{
    public BloggingContext(DbContextOptions<BloggingContext> options)
        : base(options)
    { }

    public DbSet<Blog> Blogs { get; set; }
}

At first glance everything’s fine. But you’ll have troubles, when you try to add migration with dotnet ef migrations add {NameOfMigraion}. You will have System.MissingMethodException: No parameterless constructor defined for this object problem. It can be fixed by adding custom IDbContextFactory. Custom factory allows us to construct context in the way we want to. But there is a big problem with connection string. How can we pass it to the context if application and IoC container aren’t working in the moment, when migration is being created? Official example offers us to hardcode connection string, such an awful practice.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace MyProject
{
    public class BloggingContextFactory : IDbContextFactory<BloggingContext>
    {
        public BloggingContext Create()
        {
            var optionsBuilder = new DbContextOptionsBuilder<BloggingContext>();
            optionsBuilder.UseSqlite("Data Source=blog.db");

            return new BloggingContext(optionsBuilder.Options);
        }
    }
}

I tried to find a existing solution. This blog-post offers to write some custom methods for parsing settings file and getting connection string. As for me it breaks of Single Responsibility Principle and force us for creating coupled code.

My solution

For resolving these cases I created a standalone project which works with settings and encapsulate logic for working with them in one place.

I decided to organise work through putting config files to the folder somewhere in the system and passing this folder’s path to the application through environment variables.

So, my solution contains a few simple parts. The first one encapsulates working with environment variables.

public interface IEnvironmentProvider
{
    string EnvironmentName { get;}
    string SettingsPath { get; }
}
    
public class EnvironmentProvider : IEnvironmentProvider
{
    public string EnvironmentName => Environment.GetEnvironmentVariable(ConfigurationConstants.EnvironmentVariableName);
    public string SettingsPath => Environment.GetEnvironmentVariable(ConfigurationConstants.SettingsPathVariableName);
}

public class ConfigurationConstants
{
    public static string SettingsPathVariableName => "SHAREDSETTINGS_LOCATION";
    public static string EnvironmentVariableName => "ASPNETCORE_ENVIRONMENT";
    public static string SettingsFileName => "appsettings.json";
    public static string SettingsEnviromentFileName => "appsettings.{0}.json";
}

I am sure that hardcoding string can’t be a good idea, so I like to put them to the special classes (recourses, files, etc). When I do this, I always know where these strings can be found. In this scenarion we also prevent creating hard-coded string with duplicate content.

The second part is the helper class which extracts and parses IConfigurationRoot.

using System;
using Configuration.EnvironmentProvider;
using Microsoft.Extensions.Configuration;

namespace Configuration
{
    public class SettingsProvider
    {
        public static IConfigurationRoot GetConfigurationRoot(IEnvironmentProvider environmentProvider)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(environmentProvider.SettingsPath)
                .AddJsonFile(ConfigurationConstants.SettingsFileName)
                .AddJsonFile(string.Format(ConfigurationConstants.SettingsEnviromentFileName, environmentProvider.EnvironmentName), optional: true)
                .AddEnvironmentVariables();

            return builder.Build();
        }      
    }
}

IEnvironmentProvider is being passed through constructor. So, we have some flexibility with changing it for different cases, but also have opportunity to write unit-test for GetConfigurationRoot fast.

After implementing this code BloggingContextFactory would be looking like in the snippet below.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace MyProject
{
    public class BloggingContextFactory : IDbContextFactory<BloggingContext>
    {
        public BloggingContext Create()
        {
            var configurationRoot = SettingsProvider.GetConfigurationRoot(new EnvironmentProvider());
            
            var optionsBuilder = new DbContextOptionsBuilder<BloggingContext>();
            //In my project I put "mainDb" to ConfigurationConstants class
            optionsBuilder.UseSqlite(configurationRoot.GetConnectionString("mainDb"));

            return new BloggingContext(optionsBuilder.Options);
        }
    }
}

No magic, no hard-code connection strings. Just flexibility and fun :).

You can find source code on github.

Conclusion

That’s all. I’ll be happy, If this article helps you.

Leave a Comment