18

I use the CQS pattern in my asp.net core project. Let's start with an example to better explain what I want to achieve. I created a command:

public class EmptyCommand : INotification{}

The command handler:

public class EmptyCommandHandler : INotificationHandler<EmptyCommand>
{
    public Task Handle(EmptyCommand notification, CancellationToken cancellationToken)
    {
        return Task.FromResult(string.Empty);
    }
}

The query:

public class EmptyQuery : IRequest<string>{}

The query handler:

public class EmptyQueryHandler : IRequestHandler<EmptyQuery, string>
{
    public Task<string> Handle(EmptyQuery notification, CancellationToken cancellationToken)
    {
        return Task.FromResult(string.Empty);
    }
}

and this is a simple example of how to run the command and query and invoke the Handle method from the EmptyCommandHandler and EmptyQueryHandler:

readonly IMediator _mediator;

public HomeController(IMediator mediator)
{
    _mediator = mediator;
}

public async Task<IActionResult> Index()
{
    await _mediator.Publish(new EmptyCommand());
    var queryResult = await _mediator.Send(new EmptyQuery());   
    return View();  
}

Please bear in mind that query can return other types not necessarily the string. I would like to create some kind of a bridge class e.g. MediatorBoostrapper, which allows me to run some business logic(e.g. log command/query via Logger) every time the Publish method is invoked and then invoke the public Task Handle(EmptyCommand notification,... method from the command handler. The solution must be generic, so this method would be invoked every time I run the Publish method. I also want to be able to do the same thing for the Send method.

I was thinking about the creation of the public class MediatorBoostrapper : IMediator but not sure what should be a proper implementation of the class and if my idea is good. Any ideas? Cheers

Edit

  1. I want to have an example of how to use the Behaviors to create a generic way to run some external method from the generic handler every time I Run the Send method for queries. I want to have a similar example for Publish method, which I use for sending commands.

  2. I want to have an example of how to use Polymorphic dispatch for the creation of the GenericCommandHandler and a GenericQueryHandler

I created a sample project on GitHub which can be found here You can feel free to try to extend this project with your solution.

GoldenAge
  • 2,918
  • 5
  • 25
  • 63
  • Can you explain what you're trying to do in that class? Remember what the S in CQS stands for: Separation. Don't wrap up logic in every call that should be a separate logic. If it's simple enough, just add it to your Handler. Or extend the class, but it really depends a lot on what it is that it needs to do. – Ben Dec 22 '18 at 12:39
  • 1
    https://github.com/jbogard/MediatR/wiki/Behaviors – Peter Bons Dec 22 '18 at 12:54
  • I want to create some kind of middleware so I can log each command and query using the logger – GoldenAge Dec 22 '18 at 13:00
  • In this case, I want to respect the DRY rule. – GoldenAge Dec 22 '18 at 13:06
  • 1
    Peter this is pretty interesting, need to play with this approach and see what i can achieve – GoldenAge Dec 22 '18 at 13:17
  • Behaviours won't work for Publish because they don't wrap INotificationHandlers. Out of curiosity why are you using INotificationHandlers for handling Commands? An IRequestHandler doesn't have to send a response and can be used for the strict Command pattern. – Steve Dec 28 '18 at 21:30

2 Answers2

8

This time I want to answer the question starting from the end.

2.

TL;DR Polymorphic Dispatch cannot be used for the CQS

After some time of playing with the MediatR library, reading the comments under my Question and consultation with my friend, I found the Polymorphic Dispatch(PD) can be used to create a generic handler only in case of the Commands. The PD solution cannot be implemented for Queries. Based on the Documentation, the handlers are contravariant and not covariant. This means the PD works only in the case where the TResponse is a constant type. In case of the Queries, this is false and each Query handler can return a different result.

I also found this issue. I think it's interesting to know you can use the Polymorphic Dispatch only if your container supports it.

1. Behaviors is the one and only solution for CQS when using the MediatR. Based on the comment under my question from #Steve and comment from jbogard I've found the way how to use Behaviors and IRequestHandler for the strict Command pattern. The full comment:

Just to summarize the changes, there are 2 main flavors of requests: those that return a value, and those that do not. The ones that do not now implement IRequest<T> where T : Unit. This was to unify requests and handlers into one single type. The diverging types broke the pipeline for many containers, the unification means you can use pipelines for any kind of request.

It forced me to add the Unit type in all cases, so I've added some helper classes for you.

  • IRequestHandler<T> - implement this and you will return Task<Unit>.
  • AsyncRequestHandler<T> - inherit this and you will return Task.
  • RequestHandler<T> - inherit this and you will return nothing (void).

For requests that do return values:

  • IRequestHandler<T, U> - you will return Task<U>
  • RequestHandler<T, U> - you will return U

I got rid of the AsyncRequestHandler because it really wasn't doing anything after the consolidation, a redundant base class.

The example

a) The Commands management:

public class EmptyCommand : IRequest{...}

public class EmptyCommandHandler : RequestHandler<EmptyCommand>
{
    protected override void Handle(EmptyCommand request){...}
}

b) The Queries management:

// can be any other type not necessarily `string`
public class EmptyQuery : IRequest<string>{...}

public class EmptyQueryHandler : IRequestHandler<EmptyQuery, string>
{
    public Task<string> Handle(EmptyQuery notification, CancellationToken cancellationToken)
    {
        return Task.FromResult("Sample response");
    }
}

c) The sample LogginBehavior class:

public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        var requestType = typeof(TRequest).Name;
        var response = await next();

        if (requestType.EndsWith("Command"))
        {
            _logger.LogInformation($"Command Request: {request}");
        }
        else if (requestType.EndsWith("Query"))
        {
            _logger.LogInformation($"Query Request: {request}");
            _logger.LogInformation($"Query Response: {response}");
        }
        else
        {
            throw new Exception("The request is not the Command or Query type");
        }

        return response;
    }

}

d) To register the LoggingBehavior add the command

services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));

to the body of the ConfigureServices method in the Startup.cs.

e) The example of how to run sample command and query:

await _mediator.Send(new EmptyCommand());
var result = await _mediator.Send(new EmptyQuery());
GoldenAge
  • 2,918
  • 5
  • 25
  • 63
6

MediatR supports dispatching notifications to generic handlers (polymorphic dispatch). For example:

public class GenericHandler<TNotification> : INotificationHandler<TNotification> 
    where TNotification : INotification
{
    public Task Handle(TNotification notification, CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

This handler will be invoked for every notification that is published through Publish(). The same is true for requests (queries/commands). You should also take a look at behaviors.

If you're using MediatR with ASP.NET Core I suggest you use the MediatR.Extensions.Microsoft.DependencyInjection library which takes care of wiring all the handlers together.

Henk Mollema
  • 44,194
  • 12
  • 93
  • 104
  • after i added `GenericHandler` to my project and tried to run the solution I received the error: `Cannot instantiate implementation type 'HighElo.Domain.CommandHandlers.GenericHandler` do i need to add some extra configuration to the Startup.cs? – GoldenAge Dec 22 '18 at 20:47
  • @GoldenAge take look at this library: https://github.com/jbogard/MediatR.Extensions.Microsoft.DependencyInjection. It handles the registration of request handlers. – Henk Mollema Dec 23 '18 at 12:54
  • I'm already using this library. I tried to register the `GenericHandler` using many different approaches e.g. `services.AddMediatR(typeof(GenericHandler));` and it still throws the same error. Not sure how should i register it.. – GoldenAge Dec 23 '18 at 14:02
  • You should only have to call `services.AddMediatR()` and it will wire all the requests and their handlers together. – Henk Mollema Dec 24 '18 at 09:43
  • So, I created a totally separate solution to test your idea, i uploaded it to GitHub and it works. When I added the same code to my project that I'm working on right now i receive this weird error that i mentioned before. You can find the example here: https://github.com/demos666/mediatr-generic-handler In my case I added `services.AddMediatR(typeof(Startup),typeof(EmptyClass));` to the Startup.cs, to make it work because I store the commands and queries in a separate project. I also need an implementation of the `GenericQueryHandler`. – GoldenAge Dec 24 '18 at 15:27
  • In my project I have the same structure of project. The StartUp project is the .Web one. The .Domain project is linked to the .Web guy. – GoldenAge Dec 24 '18 at 15:28
  • It blows at the very beginning, in the `public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); }` after i run the solution in the debug mode, im getting the:`System.ArgumentException: 'Cannot instantiate implementation type 'HighElo.Domain.CommandHandlers.GenericCommandHandler1[TNotification]' for service type 'MediatR.INotificationHandler1[TNotification]'.' ` – GoldenAge Dec 24 '18 at 15:31
  • @GoldenAge did you try using `services.AddMediatR()` without explicitly specifying the assemblies? By default it will scan all the assemblies. – Henk Mollema Dec 27 '18 at 10:54
  • See [this gist](https://gist.github.com/henkmollema/fa4ef9a15167b8aad6aa09028f849cd1) for reference. – Henk Mollema Dec 27 '18 at 11:06
  • yes I tried, it works only in case if you store everything in one project – GoldenAge Dec 27 '18 at 14:54
  • MediatR upgrade from version 5.1.0 to 6.0.0 solved above issue. – GoldenAge Jan 04 '19 at 15:58