.NET 5 is shaping up to be an excellent release for .NET folks, and C# developers are fortunate to get more features to help solve day to day programming problems and niche optimizations. One of the features that caught my eye was Module Initializers. While not new to .NET as a platform, .NET 5 and C# 9 introduces developers to the concept through the ModuleInitializerAttribute class.

This post will explore how to use the ModuleInitializerAttribute, some scenarios, and some pitfalls.

Background

Module initialization is not new to .NET as a platform, but C# developers have not had clear access to it for their applications. Assemblies are the minimum unit of deployment in the .NET Framework, and while modules and assemblies are technically two different things, with the ModuleInitializerAttribute, we can think of them as logically the same. Additionally, module initializers have no restrictions as to what we can call within these methods. That said, there are strict rules around module initialization. Let’s look at some of those rules found in Microsoft docs:

  1. A module initialization method must be static.
  2. The method must be parameterless.
  3. The method signature must be void or async void.
  4. The method cannot be generic or contained in a generic type.
  5. The method must be accessible in the module using public or internal.

The .NET runtime makes some guarantees around module initialization that folks should consider when determining this approach as a viable solution. Here are the rules directly from the spec.

  1. A module initializer is executed at, or sometime before, first access to any static field or first invocation of any method defined in the module.
  2. A module initializer shall run exactly once for any given module unless explicitly called by user code.
  3. No method other than those called directly or indirectly from the module initializer will be able to access the types, methods, or data in a module before its initializer completes execution.

Use Cases

Why would we want to use a module initializer, and what are some use cases?

The first obvious answer is we need to initialize variables and our application state before our application has an opportunity to start. Module initialization can help us avoid deadlocking and startup race conditions that are hell to debug.

Imagine we could resolve our dependencies once at the start of our application lifetime. While there would be a startup cost, the benefits for runtime performance would be significant. Resolving dependencies once during startup can be important for unit testing fixtures, web applications, and mobile applications.

In theory, module initialization could lead to more secure applications, where environmental variables are determined and locked at startup. In the case that our application detects a vulnerability, the application could decide not to start. Module initializers are an ideal place for a startup routine where we can make our checks.

Code Samples

Let’s take a look at how .NET developers can use module initializers. We will need the latest .NET 5 SDK.

On another important note, this is a C# 9 feature. Meaning .NET applications that target older runtimes (.NET Core 3.1) can still take advantage of this feature. We only need to compile our app using the .NET 5 CLI, but we can target any runtime.

To get started, we’ll need to reference the System.Runtime.CompilerServices namespace. While considering the previous sections’ rules, we need to decorate a public static method with the ModuleInitializerAttribute class.

using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

namespace Preview
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(Name);
        }

        public static string Name;

        [ModuleInitializer]
        public static void Init()
        {
            Name = "Khalid";
        }
    }
}

The expected and resulting output is not surprising.

Khalid

We can also have async initializers. Remember, we can call anything, including making network calls and running asynchronous tasks. We should note that the module initializer method has a static async void method definition.

using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

namespace Preview
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(Name);
        }

        public static string Name;

        [ModuleInitializer]
        public static async void Init()
        {
            Name = await GetName();
        }

        public static Task<string> GetName()
            => Task.FromResult("Khalid From Task!");
    }
}

Again, our result is unremarkable, but none the less exciting.

Khalid From Task!

There aren’t any limits to how many ModuleInitializer decorated methods can be in any one module. Let’s see what happens when we have two decorated initializing methods.

using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

namespace Preview
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(Name);
        }

        public static string Name;

        [ModuleInitializer]
        public static void InitOne()
        {
            Name = "Khalid";
        }
        
        [ModuleInitializer]
        public static void InitTwo()
        {
            Name = "Nicole";
        }
    }
}

What would you expect the result to be? If you said Nicole, then you would be correct.

Nicole

The order of calls is up to the compiler.

When one or more valid methods with this attribute are found in a compilation, the compiler will emit a module initializer which calls each of the attributed methods. The calls will be emitted in a reserved, but deterministic order. Microsoft

When writing initializers, we should attempt to write them so that they are agnostic to each other. This behavior could change over time, and keeping initialization methods logically isolated is the best course of action.

Here is a final example, which was pointed out by Andrey Dynatlov from the ReSharper team. The ModuleInitializer attribute can also be placed on top of the Main method. The compiler calls our Main method twice, with some interesting side-effects.

using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

namespace Preview
{
    class Program
    {
        public static int counter = 0;
            
        [ModuleInitializer]
        public static void Main()
        {
            Console.WriteLine($"Hello world {++counter}");
        }
    }
}

Running the sample above, we get the following output.

Hello world 1
Hello world 2

It goes without saying, but this is a bad idea. This is a demonstration of what the ModuleInitializerAttribute attribute can do. We also have to abide by the rules of module initializers, which means we can’t pass in arguments to Main and that main have to be public or internal. It’s a cool demo, but I repeat, don’t do this.

C# 8 and Lower Developers

The ModuleInitializerAttribute class is only available in C# 9 compiled with the .NET 5 SDK. If you’re unable to upgrade your SDK version to .NET 5 for any reason, there are options. For developers looking for this functionality, I can point them to Simon Cropp’s Fody ModuleInit package.

Perhaps the existing third-party tooling for “injecting” module initializers is sufficient for users who have been asking for this feature.

The .NET docs say as much :)

Conclusion

.NET 5 brings with it the ModuleInitializerAttribute class, giving developers the ability to run any code first. Startup initialization has advantages both in potential runtime performance gains and security. There are likely more use cases, and with the ability to call any code, both async and sync, developers should have no issues.

A note for folks doing async initialization, remember to create CancellationToken instances so that apps don’t hang at startup indefinitely. A network is a volatile place, and developers should program accordingly.

I hope you found this post helpful, and please let me know how you will use module initializers in your applications.