Search code examples
c#genericsdependency-injectionsimple-injector

How to dispatch a call from a mediator using Simple Injector


I have the following situation:

public interface ICommand { }

public interface ICommandHandler<TCommand> where TCommand : ICommand
{
    void Handle(TCommand command);
}

public interface ISepsCommandHandler<TCommand> : ICommandHandler<TCommand>
    where TCommand : ICommand
{
    event EventHandler<EntityExecutionLoggingEventArgs> UseCaseExecutionProcessing;
}

public sealed class CalculateNewAverageElectricEnergyProductionPriceCommandHandler
    : BaseCommandHandler,
    ISepsCommandHandler<CalculateNewAverageElectricEnergyProductionPriceCommand>
{ ... }

public sealed class CalculateCpiCommandHandler
    : BaseCommandHandler, ISepsCommandHandler<CalculateNewConsumerPriceIndexCommand>
{ ... }

In controllers I have multiple CommandHandlers and QueryHandlers in constructor and I would like to shorten it to a mediator pattern like with MediatR.

public interface ICqrsMediator // <TCommand, TQuery, TQueryResult>
                               // where TCommand : ICommand
{
    void Send(ICommand command);
}

public class CqrsMediator : ICqrsMediator // <ICommand
                                          // where TCommand : ICommand
{
    private readonly IDictionary<Type, ICommandHandler<ICommand>> _commands;

    public CqrsMediator(
        IEnumerable<ICommandHandler<ICommand>> commands) { ... }
    ...    
}

Problem:

I would like to resolve a collection of ICommandHandler into CqrsMediator's constructor. I have tried a variety of ways.

Question(s): Why doesn't this work in SI?

var bla = GetAllInstances(typeof(ICommandHandler<ICommand>));

I get a message that it cannot find ICommandHandler<ICommand> but that ICommandHandler<TCommand> is registered, although I have given a constraint in generics that TCommand can only be of type ICommand.

Can anybody help with the construction of the mediator pattern for CommandHandlers and QueryHandlers?


Solution

  • Why doesn't this work in SI?

    This doesn't work for the same reason as this doesn't work in .NET. This would only work when your ICommandHandler<T> interface would be defined as covariant, but that is impossible because the TCommand is an input argument.

    Let's remove the DI Container from the picture for a moment. Using plain-old C# code, the following is what you would like to accomplish:

    ICommandHandler<ICommand> handler1 = new Command1Handler();
    ICommandHandler<ICommand> handler2 = new Command2Handler();
    ICommandHandler<ICommand> handler3 = new Command3Handler();
    
    IEnumerable<ICommandHandler<ICommand>> handlers = new[] { handler1, handler2, handler3 };
    
    new CqrsMediator(handlers);
    

    The previous snippet creates three new command handlers:

    • Command1Handler implements ICommandHandler<Command1>
    • Command2Handler implements ICommandHandler<Command2>
    • Command3Handler implements ICommandHandler<Command3>

    Because you want to inject them into the CqrsMediator, you place them in variables of type ICommandHandler<ICommand>. This way you can easily construct an array (ICommandHandler<ICommand>[]), which can be injected into the CqrsMediator.

    This code, however, does not compile. The C# compiler will state the following:

    Error CS0266: Cannot implicitly convert type 'Command1Handler' to 'ICommandHandler<ICommand>'. An explicit conversion exists (are you missing a cast?)

    This is the source of your problem. Your command handlers can't be cast from, for instance, ICommandHandler<Command1> to ICommandHandler<ICommand>. To understand this, you need to learn about covariance and contravariance. You might want to start here.

    To allow ICommandHandler<Command1> to be assignable to an ICommandHandler<ICommand> it requires the ICommandHandler<TCommand> abstraction to be covariant:

    public interface ICommandHandler<out TCommand> where TCommand : ICommand { ... }
    

    In other words, you need to make TCommand an out argument.

    But this can't be done, because TCommand is actually an in argument (thus contravariant).

    Long story short, due to the way variance and generics works in .NET (and I would actually say: in math) this is impossible.

    There are two simple solutions for your problem, however.

    1. Make the CqrsMediator.Send method generic:
    public class CqrsMediator : ICqrsMediator
    {
        private readonly Container container;
    
        public CqrsMediator(Container container) => this.container = container;
    
        public void Send<TCommand>(TCommand command)
        {
            var handler = this.container.GetInstance<ICommandHandler<TCommand>>();
            handler.Handle(command);
        }
    }
    
    1. Alternatively, if making the Send method generic is not an option, you can also use reflection inside the Send method to find the command's correct type:
    public class CqrsMediator : ICqrsMediator
    {
        private readonly Container container;
    
        public CqrsMediator(Container container) => this.container = container;
    
        public void Send<TCommand>(TCommand command)
        {
            var handlerType = typeof(ICommandHandler<>).MakeGenericType(command.GetType());
            dynamic handler = container.GetInstance(handlerType);
            return handler.Handle((dynamic)command);
        }
    }
    

    In both cases you let the CqrsMediator implementation depend on the Container. This means the implementation becomes an infrastructural component; it becomes part of your Composition Root.