Environment Variables and Configuration in ASP.NET Core Apps

Working with the local environment, configuration and settings of an application is a pretty common development task. Let’s have a look how this can be done with applications targeting the cross-platform .NET Core framework.

If you’ve worked with the full .NET framework so far, you might run into some issues in .NET Core. A lot has been changed in the new cross platform world. Fortunately, the dotnet team is working really hard on closing gaps and to bring a lot of functionality back into the BCLs (base class libraries, System.*).

Application Paths and Information

One common question when writing console or web apps is, what is the directory the app is running in.

Depending on the type of the application and which API is used, asking for the application's base directory can return different results. For console apps, this should be the bin folder, which contains the EXE file or DLL. Depending on the project and build configuration, the path to the bin folder may vary.

For web apps, the base directory is usually the project's directory at debug time, or the directory you published the website into.

Targeting .NET 4.x you'd probably use the AppDomain object like

var basePath = AppDomain.CurrentDomain.BaseDirectory;
var appName = AppDomain.CurrentDomain.ApplicationIdentity.FullName;

In .NET Core, AppDomain.CurrentDomain does not exist anymore, but there are replacements for some things.

Version 1.x of ASP.NET Core ships with the Microsoft.Extensions.PlatformAbstractions package. The static class instance Microsoft.Extensions.PlatformAbstractions.PlatformServices.Default.Application gives us the following information:

var basePath = PlatformServices.Default.Application.ApplicationBasePath; 

// the app's name and version
var appName = PlatformServices.Default.Application.ApplicationName;      
var appVersion  = PlatformServices.Default.Application.ApplicationVersion; 

// object with some dotnet runtime version information
var runtimeFramework = PlatformServices.Default.Application.RuntimeFramework;  

Now, that being said, the package will be removed soon1 and most of the functionality is already available in the BCLs anyways. This means we should not be using Microsoft.Extensions.PlatformAbstractions anymore!

To replace PlatformAbstractions, and to write code which hopefully will survive an upgrade to the next platform version, we can use the following:

  • To get the app's base path, use System.AppContext.BaseDirectory

    AppContext.BaseDirectory, PlatformServices.Default.Application.ApplicationBasePath or AppDomain.CurrentDomain.BaseDirectory should all return the same.

  • Directory.GetCurrentDirectory() can be used, too. Although, this will return the project's directory instead of the bin folder in debug F5 mode for example.

Web Apps

Web applications are a little bit special as there are two locations the app needs to work with. The "bin" folder, where compiled views and other dlls will be published into, and the wwwroot folder, which contains static content and such.

In ASP.NET Core MVC, the IHostingEnvironment keeps track of those two locations. The interface has two properties, WebRootPathand ContentRootPath. ContentRootPath is actually the bin folder, naming is hard ;)

Important to note that both paths can be configured while building the web host

var host = new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseWebRoot("notusingwwwroot")
                ...

The IHostingEnvironment gets injected to the Startup and can be used from there on.

Application Name and Version

  • To get the application's name and version, the suggested solution is to use the assembly information

    • Assembly.GetEntryAssembly().GetName().Name
    • and Assembly.GetEntryAssembly().GetName().Version.ToString()
  • For the runtime framework, there is not really a good solution for now.

    We can try to use the System.Runtime.Versioning.FrameworkName, but to initialize an instance, you'd need the string representation including the version number. To get there, AppContext.TargetFrameworkName was intended to resolve this I guess, but it returns null targeting .netstandard1.6 or netcoreapp1.1.

    Alternative could be to use the new attribute TargetFrameworkAttribute. This attribute gets added by the compiler and might not exist all the time!

    string name = null;
#if !NETCORE
    name = AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName;
#endif
    name = name ?? Assembly.GetEntryAssembly()
        .GetCustomAttribute<TargetFrameworkAttribute>()?.FrameworkName;
            

    if(!string.IsNullOrEmpty(name))
        var version = new FrameworkName(name);

But this means we can rewrite the code from above to use:

var basePath = AppContext.BaseDirectory;
var appName = Assembly.GetEntryAssembly().GetName().Name;
var appVersion = Assembly.GetEntryAssembly().GetName().Version.ToString();

Hint: System.AppContext is also available since .NET 4.6, no #if needed!

Runtime Information

To figure out on which platform the application is actually running on, at runtime, we can use System.Runtime.InteropServices.RuntimeInformation. The following would check for Windows, OSX and Linux.

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
    // do something.
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
    // do something else
}

It also gives us convenient information about the OS platform, version and architecture:

// Runtime Info:
Console.WriteLine($@"
    FrameworkDescription:   {RuntimeInformation.FrameworkDescription}
    OSDescription:          {RuntimeInformation.OSDescription}
    OSArchitecture:         {RuntimeInformation.OSArchitecture}
    ProcessArchitecture:    {RuntimeInformation.ProcessArchitecture}
");

RuntimeInformation is not specific to .NET Core and is available since .NET 4.5.2.

Environment Variables and Configuration

When writing applications, the requirement to make it configurable usually comes up pretty quickly. It might need environment specific switches, logger settings or connection strings for example.

Environment variables and files are usually the first thing in mind to configure the application.

Environment.GetEnvironmentVariable can be used to retrieve variables by name or get a list of all of them.

var path = Environment.GetEnvironmentVariable("PATH");

// get all
var enumerator = Environment.GetEnvironmentVariables().GetEnumerator();
while (enumerator.MoveNext())
{
    Console.WriteLine($"{enumerator.Key,5}:{enumerator.Value,100}");
} 

The methods are available on any platform and easy to use. But to read and manage the configuration, to fall back to constants in code or another file for example, a lot of custom code would be needed.

What about configuration files?

In .NET 4.x the ConfigurationManager was responsible to load and bind configuration files like app/web.config. But in .NET Core, this doesn't exist anymore. To open and read files manually might not be a big deal, but validating and parsing them would again require a lot of custom code.

The alternative to all of this, and in a much more convenient way, is Microsoft.Extensions.Configuration.

Taking a look at the the default templates of an ASP.NET Core MVC application, the Startup of any website usually looks similar to this:

public Startup(IHostingEnvironment env)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
        .AddEnvironmentVariables();
    Configuration = builder.Build();
}

The exact same thing can be used in console applications, too, by installing the necessary NuGet packages!

  • Microsoft.Extensions.Configuration
  • Microsoft.Extensions.Configuration.EnvironmentVariables
  • If you want to load other configuration files, JSON or XML for example, install the corresponding packages...
  • Optionally, Microsoft.Extensions.Configuration.Binder

To get access to all system and user environment variables in a console (or web) application, the configuration framework is really handy. Just create a new ConfigurationBuilder which will be used to specify what kind of configuration sources to load information from.

In this case, I'm loading the environment variables only:

var configuration = new ConfigurationBuilder()
    .AddEnvironmentVariables()
    .Build();

With access to those variables we can do the same as above.

var path = configuration.GetSection("PATH").Value;

// or get all
foreach(var env in configuration.GetChildren())
{
    Console.WriteLine($"{env.Key}:{ env.Value}");
}

The benefits from using the configuration framework are for example

  • Simple binding to POCOs. Install the Microsoft.Extensions.Configuration.Binder NuGet and use one of the binder's methods like configuration.Get<MyConfigurationPoco>();
  • Use Microsoft.Extensions.Options which also does the binding, but has an injection and reload friendly architecture
  • Work with prefixed variables and load only those by calling configuration.GetSection(prefix) or builder.AddEnvironmentVariables(prefix) in the beginning
  • Load other configuration sources (JSON, XML,...) and have the framework override the values depending on the order

The configuration framework comes with a lot other nice features like secret stores (supports User Secrets and Azure Key Vault). Secrets are values you usually do not want to see in plain text files, user and password for a database or an API token for example. You can read more about that here.

Did I miss anything, was something wrong or do you have further questions? Let me know in the comments!

Related Docs

ASP.NET Core Configuration Documentation

User Secrets Documentation