.NET Core Dependency Injection with Modules and Configuration File

.NET Core 2.0 has a build in dependency injection and it works well, but if you want to do it via configuration you have to hack around. So, my idea was to introduce a concept of modules that can be managed via configuration logic.

This idea is not new, I am big fan of Autofac and Autofac has the concept of modules.

A module is a small class that can be used to bundle up a set of related components behind a ‘facade’ to simplify configuration and deployment. The module exposes a deliberate, restricted set of configuration parameters that can vary independently of the components used to implement the module.

I could use Autofac with all its goodies, but I decided to extend the built in DI mechanism with module feature.

There are four files to enable this feature.

IModule.cs

This file represents the module facade, it contains the same two functions like the IStartup interface.

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

public interface IModule
{
    void ConfigureServices(
        IServiceCollection services,
        IConfiguration configuration);

    void Configure(IApplicationBuilder app);
}

ModulesOption.cs

This file is a wrapper for module configuration. It contains a list of modules definitions.

using System.Collections.Generic;

public class ModulesOptions
{
    public List<ModuleOptions> Modules { get; set; }
}

ModuleOption.cs

The module configuration contains a fully qualified name of the module class, including its namespace and assembly.

public class ModuleOptions
{
    public string Type { get; set; }
}

ModulesStartup.cs

This class uses little bit reflection to load the modules, registers and configures the services.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

public class ModulesStartup
{
    private readonly IEnumerable<IModule> _modules;
    private readonly IConfiguration _configuration;

    public ModulesStartup(IConfiguration configuration)
    {
        this._configuration = configuration ??
            throw new ArgumentNullException(nameof(configuration));

        ModulesOptions options = configuration.Get<ModulesOptions>();

        this._modules = options.Modules
            .Select(s =>
            {
                Type type = Type.GetType(s.Type);

                if (type == null)
                {
                    throw new TypeLoadException(
                        $"Cannot load type \"{s.Type}\"");
                }

                IModule module = (IModule)Activator.CreateInstance(type);
                return module;
            }
        );
    }

    public void ConfigureServices(IServiceCollection services)
    {
        foreach (IModule module in this._modules)
        {
            module.ConfigureServices(services, this._configuration);
        }
    }

    public void Configure(IApplicationBuilder app)
    {
        foreach (IModule module in this._modules)
        {
            module.Configure(app);
        }
    }

With this four classes you are ready to go to use a file based configuration to control the DI.

SomeFancyEmailSender.cs

Lets say you want to load the IEmailSender, that is a part of default .NET Core template, from a module. You will need to create a IEmailSender implementation, something like this.

using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

public class SomeFancyEmailSender : IEmailSender
{
    private readonly ILogger<SomeFancyEmailSender> _logger;

    public SomeFancyEmailSender(ILogger<SomeFancyEmailSender> logger)
    {
        this._logger = logger;
    }

    public async Task SendEmailAsync(
        string email, 
        string subject,
        string message)
    {
        // Call some fancy API here 
        this._logger
            .LogInformation("SomeFancyEmailSender.SendEmailAsync...");
    }
}

SomeFancyEmailSenderModule.cs

And you will need a module class to register the SomeFancyEmailSender.cs service.

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

public class SomeFancyEmailSenderModule : IModule
{
    public void ConfigureServices(
        IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddScoped<IEmailSender, SomeFancyEmailSender>(); 
    }

    public void Configure(IApplicationBuilder app)
    {
        // Configure services 
    }
}

Startup.cs

To make it whole thing work you we need to integrate the ModulesStartup in your Startup class and add config to appsettings.json.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

public class Startup
{
    private readonly ModulesStartup _modules;

    public Startup(IConfiguration configuration)
    {
        this._modules = new ModulesStartup(configuration);
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
        this._modules.ConfigureServices(services);
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseMvcWithDefaultRoute();
        this._modules.Configure(app);
    }
}

appsettings.json

{
  "Modules": [
    { "Type": "SomeFancyModules.SomeFancyEmailSenderModule, SomeFancyModules" }
  ]
}

That is a simple way to extend a default .NET Core DI with file configuration based module feature. In most cases this will work, but if you require more you can always replace default DI logic with some other, like Autofac

You can find the code used in this post on GitHub

Show comments