As a C# developer, you might have overlooked the ModuleInitializer attribute introduced in version 9 of the language. This attribute is a hidden gem in the extensive BCL APIs and designates a method that the runtime should first invoke when loading the assembly. Before delving into its practical application in class library projects, let’s clarify the difference between a module and an assembly.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

Module vs Assembly

To understand the core difference between a module and an assembly, it is important to note that a module cannot be deployed independently; instead, it is always part of an assembly.

On the other hand, an assembly serves as a logical unit that contains one or more modules and includes additional metadata that describes the types, resources, and references within the assembly. As the primary building block of a .NET application, an assembly provides the necessary information for the runtime to load and execute code. 

In this context, we can consider a class within an assembly as a module. We can apply the ModuleInitializer attribute to any method within that class, provided that the method fulfills specific prerequisites.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Let’s explore these prerequisites.

Rules of a ModuleInitializer Attribute Marked Method

To consider a method as a module initialization method, it must meet the following conditions:

  • The method must be static
  • It should not have any parameters
  • The method’s signature must be either void or async void
  • It cannot be generic or reside within a generic type
  • The method must be accessible through the module’s public or internal access modifiers

Now that we have an understanding of what qualifies a method to be a module initializer, let’s delve into its application in a class library project.

How to Use the ModuleInitializer Attribute in a Class Library

The dependency injection principle is important for writing clean and loosely coupled code. We register our dependencies at the startup or entry point of the application. However, class library projects do not have a startup or entry point. To mitigate this, we can create extension methods to register dependencies for the classes used in the project. We can then invoke these extension methods in the application startup.

However, this implementation requires exposing contracts or interfaces to the library’s consumer (the application with the startup class), which is a flaw. What happens if our library’s consumer is another class library project, such as a Unit Test project?

Let’s see how we can overcome this limitation with the help of the ModuleInitiailizer attribute by doing a mock implementation of the Notification component, which can send SMS or Email. 

Firstly, we must add the required NuGet package to use Dependency Injection in our class library project:

 dotnet add package Microsoft.Extensions.DependencyInjection 

With the dependency now installed, let’s move ahead and create our interface responsible for sending notifications:

namespace MessagingLibrary;

internal interface INotification
{
    string SendNotification();
}

We define an interface INotification with internal access modifier, as we must hide our internal implementation from the library’s consumers. This interface provides a contract for implementing classes to send notifications and includes a SendNotification() method that returns a string. 

We can proceed with implementing the INotification interface now:

namespace MessagingLibrary;

internal class Email : INotification
{
    public string SendNotification()
    {
        return "Sending Email";
    }
}

internal class Sms : INotification
{
    public string SendNotification()
    {
        return "Sending SMS";
    }
}

The MessagingLibrary namespace contains two internal classes, Email and Sms, which implements the INotification interface and provide unique implementations for the SendNotification() method.

Achieving Dependency Injection

Let’s move on to the Dependency Injection part and register the dependencies and decorate it with the ModuleInitializer attribute:

internal static class DependenciesRegistration
{
    internal static ServiceProvider DependenciesProvider { get; private set; }

    [ModuleInitializer]
    [SuppressMessage("Usage", "CA2255:The \'ModuleInitializer\' attribute should not be used in libraries",
        Justification =
            "The consumer of the library is abstracted from this because of usage of Internal Access Modifier." +
            "Only used in this project for dependency registration.")]
    internal static void RegisterDependencies()
    {
        var services = new ServiceCollection();
        services.AddScoped<INotification, Sms>();
        services.AddScoped<INotification, Email>();
        DependenciesProvider = services.BuildServiceProvider();
    }
}

The RegisterDependencies() method registers dependencies using the ModuleInitializer attribute. It abides by all the prerequisites for this attribute. It adds scoped dependencies for INotification implementations of Sms and Email to the ServiceCollection.

The dependencies are built and stored in the DependenciesProvider property, allowing access to the registered dependencies within the assembly. As this method is decorated with the ModuleInitializer attribute, this method will be executed when the library gets loaded.

Our internal implementation for sending notifications is complete. Now, we must create a public API for library consumers to use:

public class Notifier
{
    private readonly INotification? _notification;
    private readonly IEnumerable? _notificationServiceLocator;
    
    public string NotificationResult { get; }

    public Notifier(string notificationType)
    {
        _notificationServiceLocator = DependenciesRegistration.DependenciesProvider.GetServices();

        if (_notificationServiceLocator is not null)
            _notification = notificationType switch
            {
                "sms" => _notificationServiceLocator.OfType()<Sms>.FirstOrDefault(),
                "email" => _notificationServiceLocator.OfType()<Email>.FirstOrDefault(),
                _ => throw new ArgumentException("Invalid notification type")
            };
        NotificationResult = _notification?.SendNotification()!;
    }
}

The Notifier class is responsible for sending notifications. In the constructor, we retrieve available implementations from the service provider and select the appropriate implementation based on the notification type using switch expression. Then we invoke the SendNotification() method on the implementation chosen. The result is stored in the NotificationResult property.

We have created a new public API for sending notifications, which separates our internal business logic from the library consumer. To test our library, we can now use unit tests to consume its APIs:

namespace Tests;

public class NotifierUnitTest
{
    [Fact]
    public void WhenSmsIsPassedAsParameter_ThenSmsMustBeSent()
    {
        var notifier = new Notifier("sms"); 
        Assert.Equal("Sending SMS", notifier.NotificationResult);
    }

    [Fact]
    public void WhenEmailIsPassedAsParameter_ThenEmailMustBeSent()
    {
        var notifier = new Notifier("email");
        Assert.Equal("Sending Email", notifier.NotificationResult);
    }

    [Fact]
    public void WhenInvalidParameterIsPassed_ThenExceptionMustBeThrown()
    {
        Assert.Throws<ArgumentException>(() => new Notifier("invalid"));
    }
}

These test methods cover different scenarios to ensure that the Notifier class behaves correctly when different notification types are provided as parameters. The tests help ensure the expected behavior of the Notifier class and provide confidence in its functionality. All the tests are green, proving our approach works.

By utilizing the ModuleInitalizer attribute, we were able to initiate the code within the method where all dependencies were registered with the service provider. This accomplishment would not have been feasible without the use of this attribute since the class library lacks an entry point.

Conclusion

We’ve learned how to use the ModuleInitializer attribute on a method that the runtime executes when loading an assembly. This attribute can be helpful for executing initialization code in projects that lack entry points, such as class libraries.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!