D
Dennis Tretyakov

A cleaner way for configuring DI container in dotnet with ServiceAnnotations

DI is great, but managing the registry of dependencies might be not. With project growing it becomes harder and harder to maintain DI registry.

Maintaining the registry manually is not just a pain in the ass:

  • it’s a continuous time waste
  • it’s a popular source of bugs
  • it’s a great demotivator to create new small classes, but use the “I’ll just stick it here” approach instead

The module approach — in any form, whether it’s framework-specific modules like Autofac.Module, either using static methods like Microsoft’s extension approach (eg. AddLogging, AddMvc), doesn’t solve the problem. It’s an attempt to hide the problem, by giving an illusionary structure, at the same time all the problems remain, In addition to that we are getting logical conflicts with shared components. In the end, the only benefit of “modularity” — is that it helps to build a bigger heap of code that requires continuous manual care.

Don’t get me wrong here. There is nothing wrong with the modular approach for distributing cross-cutting components, like the same logging or MVC mentioned above. I’m talking specifically about services within a single application.

There is another popular approach — “conventional”. In case you are not familiar with it — it means scanning an assembly using reflection and programmatically registering services based on naming conventions.

That solves the manual part - that’s true, however:

  • it’s not flexible enough.
    Yes, typically it does cover, a significant percent of registrations, but it leaves out lots of details, and as know “the devil hides in details”

  • the enforced naming pattern harms semantics, or will if it doesn’t seem like that in the beginning

  • the convention’s still something that must be continuously manually taken care of

Service Annotations

This is an approach I’ve come up with several years ago and it has proven itself on a variety of projects. Finally, I’ve packed it and published it on Github and NuGet. It has quite a minimalistic API, but sufficient to solve all the problems mentioned above. It is designed for IServiceCollection so every modern IoC framework supports it.

It is based on assembly scanning but uses attributes not naming conventions and code to handle special cases.

Service Attribute

In a typical project, the attribute handles most of the registrations. On a class level, you define how it should be registered. It is required to specify a lifetime. And optionally you can specify as what type the class should be registered, defaults to itself if not specified.

examples of Service attribute usage:

[Service(ServiceLifetime.Transient)]
public class MyService: IMyService { }

// will be registered with Transient lifetime
// will be registered as MyService
// equivalent to: services.AddTransient<MyService>();
[Service(ServiceLifetime.Singleton, typeof(IMySerivce))]
public class MyService: IMyService {}

// will be registered with Singleton lifetime
// will be registered as IMyService
// equivalent to: services.AddSingleton<IMySerivce, MyService>();
[Service(ServiceLifetime.Scoped, typeof(MyService), typeof(IMySerivce))]
public class MyService: IMyService { }

// will be registered with Scoped lifetime
// will be registered as MyService and as IMyService
// equivalent to: services.AddScoped<MyService>()
//    .AddScoped<IMySerivce>(serviceProvider => serviceProvider.GetService<MyService>());

ConfigureServices Attribute

So far we have seen the static part, but there is always a set of services that require custom resolvers, or even access to configuration or some context data. Therefore there is a ConfigureServices attribute. The attributes invoke a specified static method passing IServiceCollection instance to the method as a parameter. Looks for a method named “ConfigureServices” if not other name is specified.

an example of ConfigureServices attribute usage:

[Service(ServiceLifetime.Transient), ConfigureServices(nameof(RegisterHttpClient))]
public class MyService {
  readonly HttpClient _httpContext;

  public MyService(HttpClient httpContext) => _httpClient = httpClient;

  static void RegisterHttpClient(IServiceCollection serviceCollection, IConfiguration configuration) {
    serviceCollection.AddHttpClient<MyService>(httpClient => {
      httpClient.BaseAddress = new Uri(configuration.GetConnectionString("myServiceEndpoint"));
    });
  }
}

// the Service attribute
// will be register service with Transient lifetime
// will be register service as MyService

// the ConfigureService attribute
// will invoke RegisterHttpClient method

ConfigureServices attribute could as well be used without Service attribute. It can be applied to any class, the only requirements are:

  • that method (referred by that attribute) must be static and without overloads.
  • objects specified as parameters to be passed to a scanning context. (see Setup and configuration right below)

Setup and configuration

First, install NuGet package

dotnet add package ServiceAnnotations

Importing ServiceAnnotations namespace will add an extensions method AddAnnotatedServices to a IServiceCollection interface. Which has two parameters, both are optional.

  1. An assembly to scan. Defaults to calling assembly, so typically don’t need to be specified.

  2. Action to configure scan context, where we can pass objects that would be available as parameters for ConfigureServices attribute referred methods. Like in the example above where were using IConfiguration as a parameter to get a BaseAddress for an HttpClient.
    You don’t need to explicitly add IServiceCollection, it will be available by default

...
using ServiceAnnotations;

public class Startup {
  readonly IConfiguration _configuration;

  public Startup(IConfiguration configuration) => _configuration = configuration;

  public void ConfigureServices(IServiceCollection services) {
    services.AddAnnotatedServices(context =>
      context.Add<IConfiguration>(_configuration));
  }

  ...
}

ps

I hope it will help you to keep your code cleaners.
Don’t hesitate to open issues and contribute on Github.

© 2020 - 2024, Dennis Tretyakov