Hantera ConfigurationManager bättre

Den här posten är första delen i en serie om att bemästra sin konfiguration. Introduktionen till serien återfinns här Håll koll på din konfiguration.

I den här posten ska vi fokusera på att få till en bättre hantering av den statiska klassen ConfigurationManager och få den hanteringen testbar.

Genomgående i alla exempel som följer så kommer konfigurationen från en vanlig app.config/web.config. Detta även om projekten är .Net Core och alltså skulle kunna utnyttja ny konfigurationshantering mha json-configs och environment-hantering. App.config ser ut enligt nedan i alla exempel:

<?xml version="1.0" encoding="utf-8" ?>  
<configuration>  
  <appSettings>
    <add key="RandomPersonGeneratorApiLocation" value="https://randomuser.me/api/"/>
    <add key="NumberOfFriends" value="10"/>
  </appSettings>
</configuration>  

Följande projekt finns i sin helhet att titta på här https://github.com/HeadlightAB/MasterYourConfiguration:

Beroendet till ConfigurationManager

Generellt så är det ganska vanligt att man ser följande hantering av konfiguration i systemen som man stött på under åren som man har förvaltat kod:

...
var requestUri = ConfigurationManager.AppSettings["RandomPersonGeneratorApiLocation"];  
int numberOfFriends = Convert.ToInt32(ConfigurationManager.AppSettings["NumberOfFriends"]);

var result = httpClient.GetStringAsync($"{requestUri}?results={numberOfFriends}").Result;  
...

Komplett kod återfinns här https://github.com/HeadlightAB/MasterYourConfiguration/tree/master/HelloConfigurationManager.

Att läsa konfiguration på det här sättet är lite farligt, det är lätt att adressera fel nyckel i indexet i AppSettings. Med väldigt stor sannolikhet så finns det fler ställen i lösningen där man har samma konstruktion för att läsa konfiguration. Det försvårar såklart ytterligare och man bygger på sin last av potentiella krascher i systemet, krascher som antagligen upptäcks först i runtime.

Ett väldigt enkelt sätt att få bort det fragmenterade beroendet till ConfigurationManager är att bygga en separat klass runt den. Varje nyckel får en egen property och en instans av klassen injiceras på dom ställena där man måste komma åt konfiguration. Samtidigt som man kapslar in managern i klassen så kan man med fördel låta den implementera ett interface och på så sätt kan man enkelt mocka en instans när man bygger enhetstester. Interface och klass skulle kunna se ut enligt nedan.

public interface IConfiguration  
{
    string RandomPersonGeneratorApiLocation { get; }
    string NumberOfFriends { get; }
}

public class Configuration : IConfiguration  
{
    public string RandomPersonGeneratorApiLocation => 
        ConfigurationManager.AppSettings["RandomPersonGeneratorApiLocation"];

    public string NumberOfFriends => 
        ConfigurationManager.AppSettings["NumberOfFriends"];
}

Användningen av (I)Configuration ser du ut enligt nedan

internal class Program  
{
    static void Main()
    {
        var introducer = new Introducer(new Console(), new Configuration());

        introducer.SayHello();

        System.Console.ReadLine();
    }
}

public class Introducer  
{
    ...
    private readonly IConfiguration _configuration;

    public Introducer(IConsole console, IConfiguration configuration)
    {
        ...
        _configuration = configuration;
        ...
    }

    public void SayHello()
    {
        ...
        var httpClient = new HttpClient();
        var requestUri = _configuration.RandomPersonGeneratorApiLocation;
        var numberOfFriends = _configuration.NumberOfFriends;

        var result = httpClient.GetStringAsync($"{requestUri}?results={numberOfFriends}").Result;
        ...
    }
}

Komplett kod finns här https://github.com/HeadlightAB/MasterYourConfiguration/tree/master/HelloCentralizedConfiguration.

På det här sättet blir beroendet till den statiska klassen hanterad på ett och samma ställe och alla klasser som använder sig av (I)Configuration blir testbara på ett helt annat sätt.

Nästa steg blir att göra Configuration mindre beroende av ConfigurationManager och därmed också mer testbar.

Koppla loss dig ytterligare

I det sista exemplet ovan så är allt beroende till ConfigurationManager helt inkapslat i Configuration. Det här beroende skulle man såklart vilja trycka undan ytterligare och få även den klassen testbar på ett bättre sätt än att verkligen se till att testerna har tillgång till en app.config eller web.config med förväntade värden på nycklarna. ConfigurationManager är en statisk klass och därför lite svårare att "få bort", men det finns ett ganska smidigt sätt att få till det. I koden nedan återfinns lösningen, förklaring följer efteråt.

public class Configuration : IConfiguration  
{
    private readonly Func<string, string> _configurationManager;

    public Configuration(Func<string, string> configurationManager)
    {
        _configurationManager = configurationManager;
    }

    public string RandomPersonGeneratorApiLocation => 
        _configurationManager("RandomPersonGeneratorApiLocation");

    public string NumberOfFriends => 
        _configurationManager("NumberOfFriends");
}

OK, vad har hänt här? Jo, i konstruktorn kan man injicera en funktion (Func<string, string>) som används för att returnera konfigurationen. Detta gör alltså att klassen är fri från sitt beroende till ConfigurationManager, som är undantryckt till den som skapar en instans av klassen. Det kan då se ut enligt nedan:

class Program  
{
    static void Main()
    {
        var introducer = new Introducer(
            new Console(),
            new Configuration(key => ConfigurationManager.AppSettings[key]));

        introducer.SayHello();

        System.Console.ReadLine();
    }
}

Funktionen som injiceras använder sig av ConfigurationManager för att returnera appSettings-värden. Man blir ju aldrig av med beroende helt, men ponera att man skulle använda sig av ett IoC-ramverk som skulle kunna sköta instansieringen av Configuration, då skulle man ytterligare koppla bort sig från beroenden till statiska klasser. Man kan tycka vad man vill om IoC-ramverk, men just den här lösningen blir ganska elegant. Antag att man använder det, i .Net Core inbyggda ramverket för IoC, då skulle det kunna se ut enligt nedan:

internal class Program  
{
    static void Main()
    {
        var serviceProvider = ServiceProviderConfiguration.CreateProvider();

        var introducer = serviceProvider.GetService<Introducer>();
        introducer.SayHello();

        Console.ReadLine();
    }
}

public class ServiceProviderConfiguration  
{
    public static ServiceProvider CreateProvider()
    {
        var services = new ServiceCollection()
            .AddSingleton<IConfiguration>(service => new Configuration(key => ConfigurationManager.AppSettings[key]))
            .AddSingleton<IConsole, Console>()
            .AddScoped<Introducer>();

        return services.BuildServiceProvider();
    } 
}

Komplett kod finns här https://github.com/HeadlightAB/MasterYourConfiguration/tree/master/HelloIoCConfiguration.

Längre än så här tänkte jag inte ta hanteringen av beroende till ConfigurationManager. Däremot tänkte jag visa hur enhetstestning av Configuration skulle kunna se ut.

Enhetstesta din konfiguration

Om man tittar på exemplen ovan så ser man att den versionen av (I)Configuration som tar in en funktion i konstruktorn borgar för att enhetstesta klassen. En sådan testning skulle kunna se ut enligt nedan, där xUnit och FluentAssertions används:

public class ConfigurationTests  
{
    private readonly Configuration _sut;

    public ConfigurationTests()
    {
        _sut = new Configuration(key => $"FAKE-{key}");
    }

    [Fact]
    public void NumberOfFriends_Should_Return_Expected()
    {
        var result = _sut.NumberOfFriends;

        result.Should().Be("FAKE-NumberOfFriends");
    }

    [Fact]
    public void RandomPersonGeneratorApiLocation_Should_Return_Expected()
    {
        var result = _sut.RandomPersonGeneratorApiLocation;

        result.Should().Be("FAKE-RandomPersonGeneratorApiLocation");
    }
}

I konstruktorn till testklassen injiceras en funktion som returnerar $"FAKE-{key}", vilket är resultatet som man sedan kontrollerar. Implementation i Configuration är på det här sättet helt testbar.

Vill man kan man även bygga på Configuration-klassen med någon slags validering som man kan trigga vid uppstart. Detta skulle man kunna göra genom att implementera något slags attribut som t.ex. kontrollerar så att konfigurationen finns och dess värde är skilt från null. I projektet HelloValidateConfiguration, i klassen ConfigurationValidator, återfinns ett exempel på hur en sådan validering skulle kunna se ut. Där använder man attributen för custom configuration sections, som finns klara i .Net-ramverket. Properties märks upp med attribut i IConfiguration för att säkerställa korrekta värden.

Den uppmärksamme kanske redan har slagits av att man kan utnyttja lösningen med att injicera en Func<T1,T2,...Tx,out TResult> i många andra situationer. Antag att man har kod som är beroende av en klocka som man vill kunna enhetstesta. Man kan då injicera DateTime(Offset).Now som en Func<out TResult> där TResult alltså är DateTime(Offset).

Summering

I och med ovanstående "resa" som vi låtit ConfigurationManager ha gjort så har vi lagt en bra grund till att hantera konfiguration med hjälp av externa system, såsom Azure Key Vault eller Vault från HashiCorp. Mer om detta i den andra och sista delen i den här serien http://blog.headlight.se/hantera-dina-kansliga-konfigurationer/.

Introduktionen till serien hittar ni här Håll koll på din konfiguration, introduktion.