Creating a DI based ASP.Core or other NET application includes Dependency injection but why do we have to register all services on our own?
Introduction
In an ASP.Core application (this is not limited to ASP.Core) when we want to use the build in DI container, we need to create services and then register them in the Startup.cs's ConfigureServices
method. I wanted to streamline that and take the old M.E.F approach in fully self registering services.
The approach is easy:
- Annotate your service with either the
[Service]
or the [SingeltonService]
attribute - Call
ServiceLocator.LoadServices
- Profit?
Background
The idea is from the now obsolete Method Extension Framework (M.E.F.) that was .Nets (First?) approach to DI containers where such annotation based service discoveries were popular.
Using the Code
First let's take a look into both attributes:
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class SingeltonServiceAttribute : ServiceAttribute
{
public SingeltonServiceAttribute(params Type[] registerAs) : base(registerAs)
{
}
}
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class ServiceAttribute : Attribute
{
public ServiceAttribute(params Type[] registerAs)
{
RegisterAs = registerAs;
}
public IEnumerable<Type> RegisterAs { get; set; }
}
They are both attributes that represents our markers for the service, but also include a list of Type
to express all types the declaring type should be registered with. Now let's annotate a service:
[SingeltonService(typeof(IFooRepository), typeof(IDatabaseRepository))]
public class FooRepository : IFooRepository
{
public string Connection { get; set; }
public async Task DoStuff() { }
}
public interface IFooRepository : IDatabaseRepository
{
Task DoStuff();
}
public interface IDatabaseRepository
{
string Connection { get; set; }
}
This marks our FooRepository
as a Singleton service but also requests that IFooRepository
and also IDatabaseRepository
should point to the same instance as well. So if we request either FooRepository
or IFooRepository
or IDatabaseRepository
, we always get the SAME instance (because Singleton service).
As we now have registered the service, let's load it into the DI container with the help of our ServiceLocator
:
public static class ServiceLocator
{
public static void LoadServices(IServiceCollection services)
{
foreach (var type in AppDomain.CurrentDomain.GetAssemblies().SelectMany
(f => f.GetTypes())
.Where(e => e.GetCustomAttribute<ServiceAttribute>(false) != null))
{
var serviceAttribute = type.GetCustomAttribute<ServiceAttribute>(false);
var actorTypes = serviceAttribute.RegisterAs;
if (serviceAttribute is SingeltonServiceAttribute)
{
services.AddSingleton(type);
foreach (var actorType in actorTypes)
{
services.AddSingleton(actorType, (sCol) => sCol.GetService(type));
}
}
else
{
services.AddScoped(type);
foreach (var actorType in actorTypes)
{
services.AddScoped(actorType, (sCol) => sCol.GetService(type));
}
}
}
}
}
public class MyStartup
{
public MyStartup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
ServiceLocator.LoadServices(services);
}
}
And that's it. Calling ServiceLocator.LoadServices
starts the Service
discovery and loads all services.
Points of Interest
In bigger projects, the ConfigurateServices
method can grow quite a lot and I found it very helpful if the service that should configurate itself (DI rules) can also inject itself.
This is not exclusive to any other way of configurating services and should run fine alongside manual registrations.
History
- 30th August, 2021: Initial version