Dynamic Service Registration

Simplifying Dynamic and Consistent Service Registration in .NET Core

Asad
4 min readAug 26, 2023

--

Introduction:

In the world of .NET Core application development, managing services and dependencies is a critical task. While the built-in Dependency Injection (DI) container makes this easier, manual service registration can become a headache as your project expands. In this article, we’ll delve into a technique that uses custom attributes to dynamically and consistently register services, streamlining your codebase and simplifying the addition of new services.

Manual Registration: The Initial Approach

When you begin a project, you often register services manually in your Startup.cs class. For instance, to register a scoped service, you might use code like this:

services.AddScoped<IScopedService, ScopedService>();

Similarly, for a singleton service, you might write:

services.AddSingleton<ISingletonService, SingletonService>();

While this works perfectly well, imagine the effort needed as your application grows, requiring you to continually modify the registration code. This manual approach can lead to inconsistencies, maintenance challenges, and potentially introduce human errors.

Dynamic Registration with Custom Attributes

Enter dynamic registration: a cleaner, more maintainable approach that leverages custom attributes to streamline service registration. Let’s implement this approach step by step.

Step 1: Defining Custom Attributes

First, define custom attributes to mark your service implementations with their intended lifetimes. For instance:

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class ScopedServiceAttribute : Attribute { }

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class SingletonServiceAttribute : Attribute { }

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class TransientServiceAttribute : Attribute { }

Step 2: Creating a Common Extension Method

Next, create a common extension method that dynamically registers services based on these attributes. This method will use reflection to scan your assembly for classes marked with these attributes and register them accordingly. Here’s the code:

 public static class AttributeServiceExtension
{
public static IServiceCollection RegisterServicesWithAttributes(this IServiceCollection serviceCollection, Assembly assembly)
{
var targetServices = assembly.GetTypes()
.Where(type =>
type.GetCustomAttribute<ScopedServiceAttribute>() != null
||
type.GetCustomAttribute<TransientServiceAttribute>() != null
||
type.GetCustomAttribute<SingletonServiceAttribute>() != null
);

foreach (var serviceType in targetServices)
{
//get all interface that the class implemented
var implementedInterfaces = serviceType.GetInterfaces();

//get the lifetime of the class
ServiceLifetime lifetime = GetLifetimeFromAttribute(serviceType);

//Class implemented interface
if (implementedInterfaces != null && implementedInterfaces.Any())
{
foreach (var @interface in implementedInterfaces)
{
RegisterService(serviceCollection, @interface, serviceType, lifetime);
}
}
else
{
//Class doesn't implemented interface
RegisterService(serviceCollection, null, serviceType, lifetime);
}
}
return serviceCollection;
}
private static IServiceCollection RegisterService(IServiceCollection serviceCollection, Type? @interface, Type serviceType, ServiceLifetime lifetime)
{
_ = lifetime switch
{
ServiceLifetime.Singleton => @interface != null ?
serviceCollection.AddSingleton(@interface, serviceType) :
serviceCollection.AddSingleton(serviceType),
ServiceLifetime.Scoped => @interface != null ?
serviceCollection.AddScoped(@interface, serviceType) :
serviceCollection.AddScoped(serviceType),
ServiceLifetime.Transient => @interface != null ?
serviceCollection.AddTransient(@interface, serviceType) :
serviceCollection.AddTransient(serviceType),
_ => @interface != null ?
serviceCollection.AddScoped(@interface, serviceType) :
serviceCollection.AddScoped(serviceType),
};
return serviceCollection;
}

private static ServiceLifetime GetLifetimeFromAttribute(Type serviceType)
{
if (serviceType.GetCustomAttribute<ScopedServiceAttribute>() != null)
{
return ServiceLifetime.Scoped;
}
else if (serviceType.GetCustomAttribute<TransientServiceAttribute>() != null)
{
return ServiceLifetime.Transient;
}
else if (serviceType.GetCustomAttribute<SingletonServiceAttribute>() != null)
{
return ServiceLifetime.Singleton;
}
return ServiceLifetime.Scoped;
}
}

Step 3: Applying Dynamic Registration

Now, let’s put this extension method to work in your Startup.cs class:

public void ConfigureServices(IServiceCollection services)
{
var assembly = Assembly.GetExecutingAssembly(); // Adjust as needed

services.RegisterServicesWithAttributes(assembly);

// ... other service registrations
}

Benefits of Dynamic Registration

With dynamic registration, you gain several advantages:

1. Consistency: Service registration adheres to predefined attributes, ensuring a consistent approach throughout your codebase.

2. Maintainability: Adding new services is as simple as applying the appropriate attribute. No need to modify existing registration code.

3. Error Prevention: Automated registration reduces the likelihood of human errors when modifying registration code.

4. Scalability: This approach scales seamlessly as your application grows, maintaining its simplicity and clarity.

Demonstration: Adding a New Service

To demonstrate the power of dynamic registration, let’s consider a scenario where you want to add a new scoped service named ScopedServiceX. Here's all you need to do:

  1. Create the ScopedServiceX class and mark it with the [ScopedService] attribute.
  2. Build the solution, and the new service is automatically registered as scoped without any additional code changes.

With dynamic registration, accommodating new services is effortless and consistent.

Conclusion: A Smarter Approach to Service Registration

Dynamic service registration with custom attributes empowers you to manage dependencies more effectively. By eliminating the need for manual registration and providing a cleaner, more consistent approach, you enhance code maintainability and scalability. As your application evolves, this technique becomes an invaluable tool for maintaining a well-organized and adaptable codebase.

🔗 Explore the GitHub Project: For a deeper dive into dynamic service registration, check out the implementation and resources on GitHub

Happy coding and best regards,

Md Asadul Islam

Staff Software Engineer

LinkedIn

--

--

Asad

Passionate full-stack .NET engineer crafting seamless web solutions. Problem-solver, tech enthusiast. Let's code and innovate! 💻🚀