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.
-
An assembly to scan. Defaults to calling assembly, so typically don’t need to be specified.
-
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 aBaseAddress
for anHttpClient
.
You don’t need to explicitly addIServiceCollection
, 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));
}
...
}
P.S.
I hope it will help you to keep your code cleaners.
Don’t hesitate to open issues and contribute on Github.