Vakgebieden.

Smart Industry
Smart Health

Over ons.

Onze cultuur

Onze partners

Contact

Werken bij.

Omgevingsvariabele-jus toevoegen aan jouw .NET Core project

Sinds de opkomst van .NET Core, en in zekere mate Azure App Services, is het het eindelijk mogelijk om fatsoenlijk met environment variables (omgevingsvariabelen) te werken als bron voor configuratie.

Blog
Door: Wim-Jan van den Hoek
Publicatiedatum: 22 april 2018

Iedereen die bekend is met The Twelve-Factor App weet dat deze manier van configuratie de gewenste manier is. Waar het in kort op neerkomt, is dat informatie die in omgevingsvariabelen wordt opgeslagen, op de machine blijven waar de code draait. Dit is bijvoorbeeld erg handig wanneer je met connection strings werkt. Zogenaamde ‘secrets’ hebben, wanneer deze als fysiek bestand in het project worden opgeslagen, de neiging om te eindigen in je favoriete VCS zoals Git. Dit wordt algemeen beschouwd als een Bad Practice TM.


Voor de mensen die nog niet bekend zijn met dit principe uit The Twelve-Factor App, kijk hier eens naar: https://12factor.net/config. Lees vooral ook de overige 11 principes, er worden een hoop waardevolle inzichten gedeeld die elke developer zou moeten kennen.


Omgevingsvariabelen gebruiken in jouw applicatie

.NET Core maakt het erg eenvoudig om omgevingsvariabelen te gebruiken als configuratiebron. Over het algemeen zal iets dergelijks als dit worden gedaan in Program.cs waarin min of meer wordt verteld tegen de ConfigurationBuilder dat omgevingsvariabelen opgenomen moeten worden in de algemene configuratie:

public static IWebHost BuildWebHost(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .ConfigureAppConfiguration(builder =>
        {
            builder.AddEnvironmentVariables();
        })
        .Build();


Wanneer je een variabele wilt gebruiken zoals bijvoorbeeld: YOUR_CONNECTIONSTRING, dan kun je dit nu eenvoudig ophalen uit Configuration:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public  IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<SqlDatabaseContext>(options => 
        {
            options.UseSqlServer(Configuration["YOUR_CONNECTIONSTRING"]);
        });
    }
}

Super!


Hoewel dit uitstekend werkt is het niet de meest veilige manier. In dit voorbeeld wordt een variabele opgehaald door letterlijk aan de variabele als string te refereren in de dictionary. Dit is vrij gevoelig voor typefouten.


Gelukkig heeft .NET Core hier een handige oplossing voor door de configuratie te ‘binden’ aan een POCO-class:

// My custom class that will hold my custom configurations
public class MyConfig
{
    // This property defines my connectionstring
    public class Your_ConnectionString { get; set; }
}

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public  IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // Create a new instance of the MyConfig class
        var myConfig = new MyConfig();
        
        // Bind the configuration to my class, POCO-style
        Configuration.Bind(myConfig);

        services.AddDbContext<SqlDatabaseContext>(options => 
        {
            options.UseSqlServer(myConfig.Your_ConnectionString);
        });
    }
}


Deze manier werkt veel beter omdat het niet alleen vertelt welke instellingen er bestaan in jouw applicatie, het helpt ook erg mee aan hergebruik in je applicatie, variabele namen hoeven immers niet meer expliciet doorgegeven te worden.


Maar dit is nog steeds niet ideaal… Afgezien van het compleet verminken van je sexy code, door properties te introduceren als Your_ConnectionString (whaaargh, /rukt ogen uit kas), vinden we het fijn om dingen te gebruiken als sections om onze verschillende soorten configuraties te onderscheiden. Geen zorgen, .NET Core to the rescue!


Door wat naamgeving-magie toe te voegen aan je omgevingsvariabelen krijg je gratis dingen als sections:

# Assume the following environment variables:
DATABASE__CONNECTIONSTRING=myConnectionString


public class DatabaseConfiguration
{
    public class ConnectionString { get; set; }
}

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public  IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        var databaseConfiguration = new DatabaseConfiguration();
        Configuration.GetSection("DATABASE").Bind(databaseConfiguration);

        services.AddDbContext<SqlDatabaseContext>(options => 
        {
            options.UseSqlServer(myConfig.Your_ConnectionString);
        });

        /// etc..
    }
}

Dit werkt door de sectie naam van de variabele te scheiden met __ (of, wanneer je op Windows werkt, met een dubbele punt :, bijvoorbeeld DATABASE:CONNECTIONSTRING). Door dit voorvoegsel, wordt het automatisch herkend als section.


Dit is zeker handig te gebruiken, maar brengt nog wel wat problemen met zich mee.


Wanneer je je configuratie wilt binden aan een POCO-class, dan wordt aan de hand van de naam van de omgevingsvariabele afgeleid aan welke property deze verbonden moet worden. Oftewel, MYTESTSETTING wordt gekoppeld aan MyTestSetting. Dit is prima, maar omgevingsvariabelen krijgen over het algemeen een specifieke naamgeving door onderwerpen of woorden te scheiden met een underscore. Dus bijvoorbeeld: MAILING_DEFAULT_FROM, deze zal niet worden gekoppeld aan MailingDefaultFrom, maar wél aan Mailing_Default_From. Jammer. Een kwelling voor je ogen en wanneer dit onverhoopt toch wordt gedaan, is jouw reputatie als developer meteen VERNIETIGD. Geen recruiter die jou nog benadert op LinkedIn (klinkt als een voordeel?).


Een ander probleem met deze manier is dat het jou niet vertelt wanneer een variabele mist. Ergens, op een of ander moment, krijg je een foutmelding en dan is het aan jou de taak om uit te zoeken welke variabele er mist. Superleuk yo.


Dat kan beter!


Vroegâh

Een gangbare manier in vroegere tijden was om een class te schrijven die de ConfigurationSection overneemt waarin de benodigde configuratie properties werden beschreven. Aan de hand van de ConfigurationPropertyAttribute konden vervolgens bepaalde eisen worden gesteld aan deze instellingen:

<?xml version="1.0" encoding="utf-8"?>
<configuration>

  <configSections>
    <section name="MyApplication.MyConfiguration" type="MyApplication.MyConfiguration" />
  </configSections>

  <MyApplication.MyConfiguration
    MyCustomSetting="This Is My Setting"
  ></MyApplication.MyConfiguration>

</configuration>


public class MyConfiguration : ConfigurationSection
{
    public MyConfiguration(ConfigurationRoot root, string path) : base (root, path)
    {
    }

    [ConfigurationProperty("MyCustomSetting", IsRequired = true)]
    public string MyCustomSetting => Convert.ToString(this["MyCustomSetting"]);
}


Wanneer je vergat om de verplichte MyCustomSetting in te stellen, dan kreeg je een mooie foutmelding te zien die jou op de hoogte stelde van jouw falen. Crisis afgewend!


De tegenwoordige tijd

Gelukkig kan iets dergelijks ook nu worden gedaan. Het is niet eens erg moeilijk. Met slechts een kleine extensie op IConfiguration kunnen we iets soortgelijks voor elkaar krijgen:

public static class ConfigurationExtensions
{
    public static IConfigurationSection GetEnvironmentSection<TSection>(this IConfiguration configuration)
        where TSection : class, IConfigurationSection
    {
        // Get the type of generic TSection which is an implementation of IConfigurationSection
        var type = typeof(TSection);

        // Get the properties from the type
        var properties = type.GetProperties();
        
        // Create a new instance of our generic and insert the configuration as a parameter
        var configurationSection = (TSection) Activator.CreateInstance(typeof(TSection), configuration);

        foreach (var property in properties)
        {
            // If the property is not decorated with a ConfigurationPropertyAttribute, continue
            if (!(property
                    .GetCustomAttributes(true)
                    .FirstOrDefault(attr => attr is ConfigurationPropertyAttribute) 
                is ConfigurationPropertyAttribute
                attribute)) continue;

            // Retrieve the value from the property
            var value = configuration.GetValue<string>(attribute.Name);
            
            // Throw an exception if a setting is marked as required and is null.
            // This is the stuff we want!
            if (value == null && attribute.IsRequired) 
                throw new ConfigurationErrorsException(
                    $"Environment variable {attribute.Name} is required but is empty or not set");

            // Since an implementation of IConfigurationSection is essentially a Dictionary,
            // insert the value with the attribute name as an index
            configurationSection[attribute.Name] = value;
        }
        return configurationSection;
    }
}

Wat we hier mee kunnen, is een class schrijven die IConfigurationSection implementeert waarin beschreven wordt welke configuratie we gebruiken en uit welke omgevingsvariabele deze informatie moet worden gehaald. We kunnen dus bijvoorbeeld de volgende class schrijven:

using System;  
using System.Configuration;  
using Microsoft.Extensions.Configuration;  
using ConfigurationSection = Microsoft.Extensions.Configuration.ConfigurationSection;

public class MyConfiguration : ConfigurationSection
{
    private const string Path = "MyConfguration";

    public MyConfiguration(IConfiguration root) : base(root, Path)
    {
    }

    [ConfigurationProperty("MY_CONNECTION_STRING", IsRequired = true)]
    public string MyConnectionString => Convert.ToString(this["MY_CONNECTION_STRING"]);

    [ConfigurationProperty("MY_BOOLEAN_SETTING", IsRequired = false]]
    public bool MyBooleanSetting => Convert.ToBoolean(this["MY_BOOLEAN_SETTING"]);
}

Nu kunnen we een configuratie-object maken door de extensie te gebruiken die we eerder geschreven hebben:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public  IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        var myConfig = Configuration.GetEnvironmentSection<MyConfiguration>() as MyConfiguration;

        services.AddDbContext<SqlDatabaseContext>(options => 
        {
            options.UseSqlServer(myConfig.MyConnectionString);
        });

        /// etc..
    }
}


Wanneer MY_CONNECTION_STRING niet bestaat, treedt er een ConfigurationErrorsException op waarin wordt gemeld dat deze variabele mist. Naast het feit dat het je veel extra tijd op gaat leveren waarin je niet op zoek hoeft naar die missende variabele, is het ook nog eens handig als zelf-documenterende code. Je kunt meteen zien welke configuratie er gebruikt wordt binnen de applicatie.


Ten slotte

.NET Core biedt standaard veel verschillende manieren waarop je een applicatie kunt configureren en omgevingsvariabelen zijn eindelijk een fatsoenlijke optie. Standaard is het gebruik van omgevingsvariabelen erg eenvoudig om toe te passen en met een vleugje toegevoegde liefde is het mogelijk om ze op een robuuste manier toe te passen.


De broncode van dit project is te vinden op onze Github-pagina of je kunt direct gebruikmaken van de code door de NuGet-package toe te voegen aan je project.

Vragen over dit artikel?

Verstuur

Ook interessant

Smart Industry 2017 Slim om over na te denken?

Blog

Liever een behandelportaal dan een patiëntenportaal

Blog

Innovadis en virtual reality

Blog
Lees meer