DEV Community

Tim Sommer
Tim Sommer

Posted on • Originally published at timsommer.be on

Human readable Dependency Injection in .NET Core

Human readable Dependency Injection in .NET Core

In many applications and development teams, dependency injection (or DI) with inversion of control (or IoC) has become standard practice for creating better software design. It allows for loosely coupled modules , better unit tests (or even TDD) and a better implementation of SOLID principles.

Over the years we all got used to our favorite IoC frameworks; including Autofac, Ninject, Windsor and many others. But, with the release of ASP.NET Core, we now have DI built right into the framework! It is intentionally designed to have less features, lowering the learning curve for those who are unfamiliar with the concept. And, should the built-in IoC container not meet all your needs, a third-party IoC container plugin can be used quite easily.

With this post I would like to show you how you can write "human readable" dependency registrations. While this is framework agnostic, the code samples use the .NET Core DI framework.

If you are unfamiliar with dependency injection in ASP.NET Core, please review the documentation provided by Microsoft here.

So, let's dive in!

Standard registrations

We use the IServiceCollection to do registrations in the Startup class of the client application. Which looks like this:


// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IApp>(new App("switching-api"));
    services.AddDbContext<SomebContext>(o =>
                        o.UseSqlServer(SqlServerConnectionString))
    services.AddTransient<IRepository, Repository>();
}

Layered Application

As a general practice, move your registration to the responsible layer. If you have a data access layer (DAL), all registrations should be located within its boundaries. This follows the DRY (don't repeat yourself) principle and increases the level of maintainability (remember SOLID).

We can use an extension method to achieve exactly that. In the DB project, we add a file containing the following code:

public static IServiceCollection RegisterDbModule(this IServiceCollection services, string connectionString)
{
    services.AddDbContext<SomebContext>(
       options => options.UseSqlServer(connectionString));
    services.AddDbContext<ReadReplicaContext>(
       options => options.UseSqlServer(connectionString));
    services.AddTransient<IRepository, Repository>();
    services.AddTransient<IFastReadService, FastReadService>();

    return services;
}

All the DB related registrations are moved to the DAL. Our startup class is now much cleaner:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IApp>(new App("switching-api"));
    services.RegisterDbModule(SqlServerConnectionString);
}

Advantages should be clear at this point. We can use the RegisterDbModule wherever we want. Multiple clients (and multiple Startup classes) are not subject to change any more. If we need extra registrations in the DAL, we only need to change the extension method.

N-Tier and Micro-Services

If you have a lot of layers, a lot of micro services, an N-Tier or message/event driven application; the RegisterDbModule will not be enough.

Enter human readable registrations!

Moving on with the sample, we could split up the code even further:

public static IServiceCollection WithDbContext(this IServiceCollection services, string connectionString)
{
    services.AddDbContext<SomebContext>(
       options => options.UseSqlServer(connectionString));

    return services;
}

public static IServiceCollection WithFastReadReplica(this IServiceCollection services, string connectionString)
{
    services.AddDbContext<ReadReplicaContext>(
       options => options.UseSqlServer(connectionString));
    services.AddTransient<IFastReadService, FastReadService>();

    return services;
}

public static IServiceCollection WithRepositories(this IServiceCollection services)
{
    services.AddTransient<IRepository, Repository>();

    return services;
}

Which then gives you more control regarding dependency availability for each client application. With the sample above, our Startup class now looks like this:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IApp>(new App("switch-api"));

    //no fast read service required for this client
    services.WithDbContext(SqlServerConnectionString)
            .WithRepositories();

}

Or, with the fast-read service required:

public void ConfigureServices(IServiceCollection services)
{
    //fast read service required for this client
    services.WithDbContext(SqlServerConnectionString)
            .WithFastReadReplica(SqlServerConnectionString)
            .WithRepositories();
}

This is, of course, an overly simplistic sample.

If you are working on a an application where the registrations get more complex, the advantages are enormous.

Let's take a look at a bootstrap Startup class of a fairly complex application using Azure Service Bus, Azure BlobStorage and Azure functions:

public class Startup : FunctionsStartup

    public override void Configure(IFunctionsHostBuilder builder)
    {
        var config = new ConfigurationBuilder().AddEnvironmentVariables().Build();
        var connectionStore = new AzureCredentialStore(config);

        builder.Services.AddSingleton<IApp>(new App("some-app"));

        builder.Services
               .PerformDefaultRegistrationForFunction(connectionStore)
               .WithAzureBlobStorage(typeof(AzureBlobContainer).Assembly)
               .WithDbContext(connectionStore)
               .WithRectificationTraceService()
               .WithRetryHandling()
               .WithEventModule()
               .WithEntityCollectors()
               .WithMarketExport();

        builder.Services.AddLogging();

    }
}

As you can see, the code is very clear and transparent. It only takes a second to analyze what will be available for this client during its run-time.

Hope you enjoyed the read and happy coding!

Photo by Markus Spiske on Unsplash

Top comments (0)