Best Practices for Registering Multiple Implementations of the Same Interface in ASP.NET Core

Nov 19, 2025 · Programming · 13 views · 7.8

Keywords: ASP.NET Core | Dependency Injection | Interface Implementation | Service Registration | Factory Pattern

Abstract: This article provides an in-depth exploration of techniques for registering and resolving multiple implementations of the same interface in ASP.NET Core's dependency injection container. Through analysis of factory patterns, delegate resolvers, and other core methods, it details how to dynamically select specific implementations based on runtime conditions while addressing complex scenarios like constructor parameter injection.

In ASP.NET Core application development, scenarios frequently arise where multiple implementations of the same interface need to be registered. Unlike traditional IoC containers such as Unity, ASP.NET Core's built-in dependency injection container does not directly provide functionality for registering services by key. This article systematically analyzes this problem and offers multiple practical solutions.

Problem Background and Core Challenges

Assume we have a base service interface IService with three different implementation classes:

public interface IService { }
public class ServiceA : IService { }
public class ServiceB : IService { } 
public class ServiceC : IService { }

In traditional dependency injection containers, different implementations are typically distinguished by keys. However, in ASP.NET Core's default DI container, methods like AddTransient, AddScoped, and AddSingleton do not support key parameters. This creates the challenge of being unable to directly select the appropriate implementation based on specific conditions at runtime.

Delegate Resolver Pattern

The most elegant solution is the delegate resolver pattern. This approach encapsulates service resolution logic within a dedicated delegate type, maintaining code clarity while avoiding violations of the Dependency Inversion Principle.

First, define the resolver delegate:

public delegate IService ServiceResolver(string key);

Register services in Startup.cs or Program.cs:

services.AddTransient<ServiceA>();
services.AddTransient<ServiceB>();
services.AddTransient<ServiceC>();

services.AddTransient<ServiceResolver>(serviceProvider => key =>
{
    switch (key)
    {
        case "A":
            return serviceProvider.GetService<ServiceA>();
        case "B":
            return serviceProvider.GetService<ServiceB>();
        case "C":
            return serviceProvider.GetService<ServiceC>();
        default:
            throw new KeyNotFoundException();
    }
});

Usage in controllers or other DI-enabled classes:

public class Consumer
{
    private readonly IService _service;

    public Consumer(ServiceResolver serviceAccessor)
    {
        _service = serviceAccessor("A");
    }

    public void UseService()
    {
        _service.DoTheThing();
    }
}

Handling Constructor Parameter Injection

In practical applications, service implementation classes often require specific configuration parameters. For example:

public class ServiceA : IService
{
    private string _efConnectionString;
    
    public ServiceA(string efConnectionString)
    {
        _efConnectionString = efConnectionString;
    }
}

public class ServiceB : IService
{
    private string _mongoConnectionString;
    
    public ServiceB(string mongoConnectionString)
    {
        _mongoConnectionString = mongoConnectionString;
    }
}

For such cases, provide necessary parameters through factory methods during service registration:

services.AddTransient<ServiceA>(provider => 
    new ServiceA(configuration.GetConnectionString("EFConnection")));

services.AddTransient<ServiceB>(provider => 
    new ServiceB(configuration.GetConnectionString("MongoConnection")));

GetServices Method Alternative

Another approach uses the GetServices method to retrieve all registered implementations, then selects specific instances via LINQ queries:

services.AddSingleton<IService, ServiceA>();
services.AddSingleton<IService, ServiceB>();
services.AddSingleton<IService, ServiceC>();

// Resolution where needed
var services = serviceProvider.GetServices<IService>();
var serviceB = services.First(o => o.GetType() == typeof(ServiceB));

While simple, this method suffers from poor performance with many services and lacks explicit key mapping.

Interface Refactoring Strategy

From a design pattern perspective, create specialized interfaces for each concrete implementation class:

public interface IServiceA : IService { }
public interface IServiceB : IService { }
public interface IServiceC : IService { }

Corresponding implementation classes implement these specialized interfaces:

public class ServiceA : IServiceA { }
public class ServiceB : IServiceB { }
public class ServiceC : IServiceC { }

Service registration:

services.AddTransient<IServiceA, ServiceA>();
services.AddTransient<IServiceB, ServiceB>();
services.AddTransent<IServiceC, ServiceC>();

This approach, while increasing interface count, provides the clearest dependency relationships and adheres to the Single Responsibility Principle.

.NET 8 Keyed Services Feature

For developers using .NET 8 and later, leverage the built-in keyed services functionality:

// Service registration
builder.Services.AddKeyedSingleton<IService, ServiceA>("A");
builder.Services.AddKeyedSingleton<IService, ServiceB>("B");
builder.Services.AddKeyedSingleton<IService, ServiceC>("C");

// Usage in constructors
public class Consumer
{
    public Consumer([FromKeyedServices("A")] IService service)
    {
        _service = service;
    }
}

This represents the most official and elegant solution, recommended for priority consideration in new projects.

Performance and Design Considerations

When selecting a specific approach, consider these factors:

Practical Application Scenarios

The multiple implementation registration pattern proves particularly useful in these scenarios:

By appropriately applying these technical solutions, developers can flexibly handle multiple implementations of the same interface in ASP.NET Core, building more robust and extensible application architectures.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.