Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET / ASP.NET-Core

Completely Selfconfigurating Service with .NET 5

5.00/5 (6 votes)
30 Aug 2021CPOL1 min read 12.9K  
Why do we need to register all services with service.AddService?
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:

  1. Annotate your service with either the [Service] or the [SingeltonService] attribute
  2. Call ServiceLocator.LoadServices
  3. 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:

C#
[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:

C#
[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:

C#
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; }

    
    // This method gets called by the runtime. 
    // Use this method to add services to the container.
    // For more information on how to configure your application, 
    // visit https://go.microsoft.com/fwlink/?LinkID=398940
    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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)