Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save stormwild/6bdf94b7eefb800e8874226c3d82e64f to your computer and use it in GitHub Desktop.
Save stormwild/6bdf94b7eefb800e8874226c3d82e64f to your computer and use it in GitHub Desktop.
MediatR vs MassTransit Mediator / Part 1 / Differences

Why mediator?

In a ASP.NET Web application. You don't need mediator

  • If you prefers to make controlers depends directly to the Application Codes instead of indirectly via the Mediator.
  • If you prefers to make a normal ASP.NET Web Application instead of a Mediator Application

Frankly it is not a Bad choice, no need to use a Mediator framework or make a Mediator Application just because everyone did.. Though, There are benefits in making a Mediator Application:

  • Event sourcing (Messages broadcast), CQS pattern..
  • Decouple the Controler (presentation) from Application codes, so that you could swap the presentation technology. For eg, if you make a "MassTransit" application, then you can swap the presentation layer to Mediator or RabbitMQ, or Grpc.. => you are not to be sticked with or limited by ASP.NET presentation => but rather sticked with and limited by your mediator framework!
  • You also get some features from the Mediator framework, which you might no have in a "normal" ASP.NET application. In case MassTransit:

In this article I will compare 2 Mediator implementation: MassTransit Mediator and MediatR.

  • The "sender-side" is where we send the request and try to get back the response. Typicaly in the ASP web application sender are the Controllers.
  • The "consumer-side" is where the requests are received and handled (or consumed) then return (a) response(s).

MediatR basic usages

  • Request-Reply
public class MyCommand : IRequest<MyResponse> {..}

class Consumer: IRequestHandler<MyCommand, MyResponse> 
{
    public async Task<MyResponse> Handle(MyCommand input, CancellationToken cancellationToken) 
    {
        MyResponse output = ...;
        return output;
    }
}
  • Message Broadcast
public class MyNotif : INotification {..}

class Consumer: INotificationHandler<MyNotif>
{
    public async Task Handle(MyNotif input, CancellationToken cancellationToken) {...}
}
  • On the sender-side:
mediator.Send(new MyCommand());
mediator.Publish(new MyNotif());

MassTransit Mediator basic usages

There is no different between "Request/Reply" and "Message broadcast":

  • The message is always broadcasted to all the consumers.
  • If the sender wait for a response then we got "Request-Reply" communication
  • If the sender don't wait for the response we got "Message broadcast" communication
public class MyCommand {..}

class Consumer: IConsumer<MyCommand> 
{
    public async Task Consume(Context context) 
    {
        MyCommand input = context.Message;
        context.Response(new MyResponse()); 
        context.Response(new MyError());
    }
}

On the sender side, we broadcast the request (Broadcast communication) and at the same time we can wait for the first response coming back (Request-Reply pattern)

  • Request-Reply communication:
var requestClient = mediator.CreateRequestClient<MyCommand>()
requestClient.GetResponse<MyResponse, MyError>(new MyCommand());
if (response.Is(out Response<MyResponse> myResponse))
{
    // do something with myResponse
}
else if (response.Is(out Response<MyError> myError))
{
    // do something with myError
}
  • Broadcast communication:
mediator.Publish(new MyCommand()); //don't care if the command is not consumed
//or
mediator.Send(new MyCommand()); //crash if the command is not consume by any consumer

Diffrences

  • MediatR: the input payload must to implement IRequest or INotification empty interface.

  • MassTransit: the input payload is normal POCO class.

  • In case request/reply communication

    • The sender-side crash if the consumer crash.
    • MediatR: you sent 1 TRequest and get back only 1 TResponse, a request type is force attached to a response type. It means that if the request is consumed (the consumer codes is executed) then the sender-side will surely got a response in the declaration type.
    • MassTransit: you sent 1 TRequest and get back multiple TResponse1, TResponse2.. the consumer can respond anything.. It means that if the request is consumed (the consumer codes is executed) then
      • the consumer might "forget" to publish the response => the sender-side will get a TimeOut after 30s of waiting
      • the consumer might publish a TReponse3 while the sender is waiting for TResponse1 or TResponse2 => the sender-side will get a TimeOut after 30s of waiting
    • In case there are multiple Consumer1, Consumer2 to handle a Request
      • MediatR: the DI framework choose 1 Consumer to be executed.
      • MassTransit: All the Consumers are executed, the Sender takes the first response coming out.
  • In case Message broadcast to multiple Consumer1, Consumer2

    • MediatR: if one of the Consumer crash then the sender side crash (I think it is a bad behaviour)
    • MassTransit: just broadcast MyCommand, and don't care if some Consumers crashed (I think it is the right behaviour)

Middleware MediatR

Straight-forward..

public class FooMiddleware<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        Console.WriteLine($"Before calling request {typeof(TRequest).Name}");
        var response = await next(); //we can capture exceptions on Consumer here
        Console.WriteLine($"After getting response  {typeof(TResponse).Name}"); //we can manipulate the response computed by the Consumer
        return response;
    }
}

Middleware MassTransit Mediator

public class FooMiddleware<T> : IFilter<ConsumeContext<T>> where T : class
{
    public async Task Send(ConsumeContext<T> context, IPipe<ConsumeContext<T>> next)
    {
        Console.WriteLine($"Before calling request");
        await next.Send(context); //we can capture exceptions on Consumer here
        Console.WriteLine($"After calling request"); //no idea if the consumer will response something
    }

    public void Probe(ProbeContext context) { }
}

Unlike MediatR, we don't have access to the "response" in the middleware codes (who knows if the consumers will give 1 response, 2 responses or no response). We don't evens know the type of the response because the consumer might context.Response(Anything). So the "Consumer Context middlewares" could only

  • Hook the start and the end of the Consumer codes
  • Catching exception in the Consumer codes

In order to capture the "response" we will have to rely on the "Send Context middlewares". But these middlewares are invoked for both requests and responses (all the messages "incoming or ongoing" the mediator). So if you want to log the "requests" and "responses" then you will have to combine these 2 levels of middlewares like this 👯 (the conversationId help you to match multiple responses to a request)

public class LogTryCatchConsumeFilter<T> : IFilter<ConsumeContext<T>> where T : class
{
    public async Task Send(ConsumeContext<T> context, IPipe<ConsumeContext<T>> next)
    {
        _logger.LogInformation("REQUEST {Message} {ConversationId}", context.Message, context.ConversationId);
        try
        {
            await next.Send(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Consumer crashed {Message} {ConversationId}", context.Message, context.ConversationId);
            throw;
        }
    }

    public void Probe(ProbeContext context) { }
}

public class LogResponseSendFilter<T> : IFilter<SendContext<T>> where T : class
{
    public Task Send(SendContext<T> context, IPipe<SendContext<T>> next)
    {
        if (context.DestinationAddress?.LocalPath == "/response") //here we filter (out-going) response message, and ignore incoming message
        {
            _logger.LogInformation("RESPONSE {Message} {ConversationId}", context.Message, context.ConversationId);
        }

        return next.Send(context);
    }

    public void Probe(ProbeContext context) { }
}

Testing

  • A MediatR Consumer is just a normal function with 1 input + 1 output you won't have any problem to Unit test these functions.
  • A MassTransit Consumer has 1 complicated input which is the Context and multiple Output with unknown type..

=> it is obvious that testing a MassTransit Consumer is much less straight forward! You will have to do something like this:

//Arrange
MyCommand testPayload;
var callContext = Substitute.For<ConsumeContext<MyCommand>>();
callContext.Message.Returns(testPayload);

//Act
await Consumer.Consume(callContext);

//Assert multiple responses
callContext.Received(1).RespondAsync(Arg.Any<MyResponse>());

In order to make test "easier" you could wrap your head around the MassTransit "test harness". Basicly, this tool help you to monitor / assert everything coming in and out the MassTransit mediator.. You can evens monitor if a Consumer send (broadcast) requests to others Consumers in your test.

A little benchmark

In case the Consumer do nothing, a MediatR request/reply is around 50 micro-second faster!

image

So if the Consumer actually do something then the 50 micro-second faster becomes irrelevant

image

My personal evaluation

It is obvious that MassTransit is more advance and more complicated than MediatR. I expect that Masstransit Mediator can do anything that mediatR can, (but in a more complicated way).

Your MassTransit Consumer not only works for your mediator application, but we should be able to use them to Consume messages on RabbitMQ, Amazon SQS, Azure Service Bus..

Moreover MassTransit give much more power to your Consumers:

I thought that MassTransit is overkill and we don't wanna need all of these things. In a small micro-service I would go for MediatR for an easier life, but I changed my mind while making the Part 2.

In the next part, We will try to "simplify" the MassTransit Consumer to make it as straight forward as a MediatR handler, so We could get the best of both world: Read Part 2: Making MassTransit mediator as simple as MediatR

//TODO add external reference link => at the moment readers can Google things themself

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment