Runtime Host Configuration Options and AppContext data in .NET Core

Β· 763 words Β· 4 minutes to read

One of the little known and rarely used hidden features of .NET Core is the ability to seed AppContext data dictionary directly from the csproj project file. This provides an interesting mechanism for exposing low-level tweaks/knobs/settings for your application code.

Let’s have a look.

Configuration in .NET Core πŸ”—

Via the Microsoft.Extensions.Configuration package, .NET Core has a really rich and flexible application configuration model.

While this technique discussed here is in no way a replacement for .NET Core configuration constructs, for simple, low-level cases it might be interesting to have a look at a little known feature called RuntimeHostConfigurationOption.

Runtime Host Configuration Options πŸ”—

In .NET Core, as well as in .NET Framework 4.6+, there is an AppContext type, which was designed as a mechanism to provide switch functionalities for library authors. The type is not very widely used, and if you ever interacted with AppContext, chances are you have done so to access its BaseDirectory property. It is its by far most popular API - it returns the path of the base directory that the assembly resolver uses to probe for assemblies. In fact a quick Github search reveals over 12k usages of it.

Aside from that, AppContext also comes with a static private data dictionary, which you can access via GetData(string name) method. What is particularly interesting, is that while there is a public method to get the data out of AppContext, there is no (or so it seems), way to get the data into it. This is something we will explore now, and this is where runtime host configuration options come in.

As it turns out, what you could do, is you could simply add RuntimeHostConfigurationOption entry (or entries) into your project file (csproj) and the .NET Core runtime will automatically feed them into the runtime AppContext data dictionary.

For example, consider the following program:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <RuntimeHostConfigurationOption Include="abc" Value="123" />
    <RuntimeHostConfigurationOption Include="foo" Value="bar" />
  </ItemGroup>
</Project>

When this program executes, you can read the settings from the data dictionary do the following:

    class Program
    {
        static void Main(string[] args)
        {
            var config1 = AppContext.GetData("abc");
            var config2 = AppContext.GetData("foo");
            
            // prints 123
            Console.WriteLine(config1); 
            
            // prints bar
            Console.WriteLine(config2); 
        }
    }

The way it works in practice, is that the values are not magically compiled into the assembly, but instead the dotnet SDK, at build time, using its GenerateRuntimeConfigurationFiles MsBuild task, copies them into a special ${project}.runtimeconfig.json file, which is published into the output folder. When you then execute your app, it is automatically loaded by the runtime.

The file has a simple JSON format and in my case the file looks like this, and you could find the runtime host configuration options in the configProperties:

{
  "runtimeOptions": {
    "tfm": "netcoreapp3.0",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "3.0.0"
    },
    "configProperties": {
      "abc": 123,
      "foo": "bar"
    }
  }
}

While this doesn’t really replace the richness of the configuration system, it provides a nice no-frills alternative for setting some low level flags and knobs for your code.

Programmatic writes to the AppContext data dictionary πŸ”—

Can you programmatically write data into the AppContext data dictionary?

This is actually quite an interesting story. If you look at the implementation of AppContext in the CoreFX repository, you will find out that it indeed has a SetData method. However, it is hidden on the reference assembly, meaning you can’t use it in your code, since it won’t compile.

But, knowing that it’s there at runtime - its missing just at compile time - you could always pick it up with reflection.

typeof(AppContext).GetMethod("SetData", 
    BindingFlags.Public | BindingFlags.Static)
    .Invoke(null, new object[] { "hello", "world" });
var hello = AppContext.GetData("hello");

// prints world
Console.WriteLine(hello);

This is quite baffling at first - the method is not available at compile time, but available at runtime - but we do compile our code against reference assemblies and run it against implementation assemblies so these things can and do happen.

This is of course an interesting piece of trivia, but not really the recommended usage of course. It is however possible to write to AppContext data dictionary in a typed way, and that’s via the good old AppDomain static type (even though there are no app domains in .NET Core).

AppDomain.CurrentDomain.SetData("secret", "hooray");
var secret = AppContext.GetData("secret");

// prints hooray
Console.WriteLine(secret);

var secret2 = AppDomain.CurrentDomain.GetData("secret");

// also prints hooray
Console.WriteLine(secret2);

As it turns out, the data dictionary of AppContext is actually backing the SetData/GetData of AppDomain.CurrentDomain.

There is not really a lot to see there, but if you are interested in the source code for this blog post, it’s, as always, available on Github.

About


Hi! I'm Filip W., a cloud architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github and on Mastodon.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP