Registering Multiple Implementation With Same Interface



We have an interface with name IVehicle

public interface IVehicle
{
    string Type { get; set; }
}

We have 3 Implementations of this interface Car,Van and Truck.

public class Car : IVehicle
{
    public string Type { get; set; } = "Car";
}

public class Truck : IVehicle
{
    public string Type { get; set; } = "Truck";
}

public class Van : IVehicle
{
    public string Type { get; set; } = "Van";
}

We have a CarController, where we want to use the Car implementation of IVehicle.


[Route("/api/cars")]
[ApiController]
public class CarController : ControllerBase
{
    private IVehicle _vehicle;

    public CarController(IVehicle vehicle)
    {
        _vehicle = vehicle;
    }
}

It is a usual approach to inject any service. But, in our case IVehcle have 3 implementations.However, determining which implementation is in use is not straightforward with our current method. To address this, we need to adopt a different approach.

1. Using IServiceProvider

Add these lines to the Program.cs

builder.Services.AddTransient<Car>();
builder.Services.AddTransient<Truck>();
builder.Services.AddTransient<Van>();

Update the controller.

public class CarController : ControllerBase
{
    private IVehicle _vehicle;

    public CarController(IServiceProvider serviceProvider)
    {
        _vehicle = serviceProvider.GetRequiredService<Car>();
    }
}

Pros

  • Simple to Implement: Easy to understand and straightforward to implement.
  • Direct Resolution: Directly resolves the required service.

Cons

  • Tight Coupling: The controller is tightly coupled to the IServiceProvider, which can make testing more difficult.
  • Violation of Dependency Injection Principles: It bypasses constructor injection and violates the principle of explicit dependencies.
  • Less Flexible: Not easily extensible if you need to resolve other implementations based on different conditions.

2. Using Factory

Let's modify the Program.cs file.

builder.Services.AddTransient<Car>();
builder.Services.AddTransient<Truck>();
builder.Services.AddTransient<Van>();

builder.Services.AddTransient<Func<string, IVehicle>>(serviceProvider => key =>
{
    switch (key)
    {
        case "Car":
            return serviceProvider.GetService<Car>();
        case "Van":
            return serviceProvider.GetService<Van>();
        case "Truck":
            return serviceProvider.GetService<Truck>();
        default:
            throw new KeyNotFoundException();
    }
});

Let's break it down.

  • First we are registering our Car,Truck and Van services.
  • builder.Services.AddTransient<Func<string, IVehicle>> registers a factory function that takes a string (key) and returns an IVehicle instance. The factory itself is registered as a transient service, so a new factory function is created each time it is requested.
  • serviceProvider => key => { ... } defines the factory function. This function takes a string key and uses a switch statement to determine which concrete implementation of IVehicle to return. Eg. If we pass Car as a key, this functions will returns the instance of Car.

Now, let's see how we can use this factory in our controller.

public class CarController : ControllerBase
{
    private IVehicle _vehicle;

    public CarController(Func<string, IVehicle> vehicleFactory)
    {
        _vehicle = vehicleFactory("Car");
    }
}

You can avoid the switch statement. For that you need to modify your code.

builder.Services.AddTransient<Func<string, IVehicle>>(serviceProvider => key =>
{
    if (vehicleTypeMap.TryGetValue(key, out var type))
    {
        return serviceProvider.GetService(type) as IVehicle;
    }
    throw new KeyNotFoundException($"No service registered for key: {key}");
});

.NET 8.0 Keyed services

This feature is introduced with .net 8.0. It is much simpler approach. Register your services with AddKeyedTransient (or AddKeyedScoped or AddKeyedSingleton)

builder.Services.AddKeyedTransient<IVehicle, Car>(nameof(Car));
builder.Services.AddKeyedTransient<IVehicle, Truck>(nameof(Truck));
builder.Services.AddKeyedTransient<IVehicle, Van>(nameof(Van));

In the controller,

[Route("/api/cars")]
[ApiController]
public class CarController : ControllerBase
{
    private IVehicle _vehicle;

    public CarController([FromKeyedServices(nameof(Van))] IVehicle vehicle)
    {
        _vehicle = vehicle;
    }
}

Comments

Popular posts from this blog

Asp.net core JWT authentication and role-based authorization (.NET 8.0)