Rico Suter's blog.
 


The default .NET dependency injection container Microsoft.Extensions.DependencyInjection does not provide a way to register named services. That means you cannot register an interface multiple times and then resolve a specific instance with a name or key. This article shows a technique to solve this by adding a generic marker parameter to the service interface so that it can be registered multiple times and resolved with the marker type.

Let’s start with a sample: In the code below we have two service classes, OrderService and ProductService, both of them want to publish a message to a message queue through the help of an IMessagePublisher service interface. The first, non-working implementation would look like this:

public interface IMessagePublisher
{
    void Publish(Message message);
}

public class OrderService
{
    // Error: The DI container does not know what registration he needs to inject here
    public OrderService(IMessagePublisher orderPublisher) { ... }

    public Create() { orderPublisher.Publish(new OrderCreatedMessage()); }
}

public class ProductService
{
    // Error
    public OrderService(IMessagePublisher productPublisher) { ... }

    public Create() { productPublisher.Publish(new ProductCreatedMessage()); }
}

// Register two publishers with the same interface (MyMessagePublisher is an actual implementation of IMessagePublisher)
services.AddSingleton<IMessagePublisher>(new MyMessagePublisher("MyOrderCreatedQueue"));
services.AddSingleton<IMessagePublisher>(new MyMessagePublisher("MyProductCreatedQueue"));

The previous code won’t work because the IMessagePublisher interface has been registered multiple times and the resolver does not know what instance to resolve to. The problem we have here is that the default .NET dependency container does not support named service registrations. Something like this is not possible with the default container:

public class OrderService
{
    public OrderService([Name("OrderPublisher")] IMessagePublisher orderPublisher) 
    { ... }
}

services.AddNamedSingleton<IMessagePublisher>(
    "OrderPublisher", new MyMessagePublisher("MyOrderCreatedQueue"));

Most advanced dependency injection container libraries support a form of named registrations. However, the problem with them is that you will always have some sort of dependency to the injection library; in the previous sample a dependency to the NameAttribute - this is a leaky abstraction and should be avoided.

One solution to this problem is to introduce the same interface but with an additional generic parameter. Then implement an interceptor class to wrap the original instance and “enrich” it with a generic type:

// Define interface with a generic parameter
public interface IMessagePublisher<T> : IMessagePublisher { }

public interface IMessagePublisher
{
    void Publish(Message message);
}

// Implement a publisher interceptor class which adds the generic parameter to an existing 
// publisher instance and which just calls the methods of the intercepted instance.
public class MessagePublisher<T> : IMessagePublisher<T>
{
    private IMessagePublisher _publisher;

    public MessagePublisher(IMessagePublisher publisher)
    {
        _publisher = publisher;
    }

    public virtual void Publish(Message message)
    {
        _publisher.Publish(message);
    }
}

// Now we can register specific publishers
services.AddSingleton<IMessagePublisher<OrderCreatedMessage>>(
    new MessagePublisher<OrderCreatedMessage>(
        new MyMessagePublisher("MyOrderCreatedQueue")));
services.AddSingleton<IMessagePublisher<ProductCreatedMessage>>(
    new MessagePublisher<ProductCreatedMessage>(
        new MyMessagePublisher("MyProductCreatedQueue")));

// In the services, we can now inject specific publishers
public class OrderService
{
    public OrderService(IMessagePublisher<OrderCreatedMessage> orderPublisher) 
    { ... }
}

public class ProductService
{
    public OrderService(IMessagePublisher<ProductCreatedMessage> productPublisher) 
    { ... }
}

As you can see, with the new interfaces we are able to register two publishers and inject the desired ones into the services. With the new MessagePublisher<T> interceptor class you can also enhance existing publisher implementations with additional logic by implementing methods with new aspects:

public class LoggingMessagePublisher<T> : MessagePublisher<T>
{
    private readonly ILogger _logger;

    public LoggingMessagePublisher(ILogger logger, IMessagePublisher publisher) 
        : base(publisher)
    {
        _logger = logger;
    }

    public virtual void Publish(Message message)
    {
        _logger.LogInformation("Publishing a message.");
        base.Publish(message);
    }
}

IMessagePublisher<ProductCreatedMessage> publisher = 
    new LoggingMessagePublisher<ProductCreatedMessage>(
        logger, new MyMessagePublisher("MyOrderCreatedQueue"));

To simplify applying these interceptors you can also implement some extension methods:

public static class MessagePublisherExtensions
{
    public static IMessagePublisher<T> WithMessageType<T>(this IMessagePublisher publisher)
    {
        return new MessagePublisher<T>(publisher);
    }

    public static IMessagePublisher<T> WithLogging<T>(this IMessagePublisher<T> publisher)
    {
        return new LoggingMessagePublisher<T>(publisher);
    }

    public static IMessagePublisher WithLogging(this IMessagePublisher publisher)
    {
        // Here we use object because we don't care about the type
        return new LoggingMessagePublisher<object>(publisher);
    }
}

IMessagePublisher<ProductCreatedMessage> publisher = 
    new MyMessagePublisher("MyOrderCreatedQueue")
        .WithLogging(logger)
        .WithMessageType<ProductCreatedMessage>();

In simpler cases, you can also leave out the IMessagePublisher interface and only use a IMessagePublisher<T> interface.

What do you think of this technique to circumvent the missing named registration support?

This technique is heavily used in my abstraction projects Namotion.Messaging and Namotion.Storage.



Discussion