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:
- Multiple Responses
- Retry, Outbox, Rate Limiter, Circuit Breaker... the list goes on..
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).
- 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());
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
-
MediatR: the input payload must to implement
IRequest
orINotification
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 1TResponse
, 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 multipleTResponse1
,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 forTResponse1
orTResponse2
=> the sender-side will get aTimeOut
after 30s of waiting
- the consumer might "forget" to publish the response => the sender-side will get a
- 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)
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;
}
}
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) { }
}
- 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.
In case the Consumer do nothing, a MediatR request/reply is around 50 micro-second faster!
So if the Consumer actually do something then the 50 micro-second faster becomes irrelevant
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:
- Multiple Responses
- Retry, Outbox, Rate Limiter, Circuit Breaker... the list goes on.. but attention, not all the functionalities will work with Mediator.
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
When using MediatR, broadcasting a message via the INotification interface and invoking mediatr.Publish does not inherently cause a crash if one of the consumers (notification handlers) encounters an exception. Let’s delve into the details:
INotification Interface:
The INotification interface is a marker interface used to represent notifications (events) that can be broadcasted to multiple handlers.
It doesn’t require any specific implementation or response type, making it suitable for scenarios where you want to notify multiple consumers without expecting a response.
Publishing Notifications:
When you use mediatr.Publish to broadcast a notification, MediatR invokes all registered notification handlers for that notification type.
By default, MediatR awaits each handler sequentially, ensuring that each handler runs after the previous one completes1.
If one of the handlers throws an exception, it won’t crash the entire application. Instead, the exception will propagate to the caller (the code that invoked mediatr.Publish).