In this article, we are going to explore the key differences between the Publish and Send methods in MediatR, a powerful .NET library for component communication.
We will see MediatR in action as we explore its implementation with requests, notifications, and behaviors.
Let’s start.
The Mediator Pattern
The mediator pattern is a software design pattern that enables indirect communication between software components, promoting loose coupling by introducing a mediator object as an intermediary.
By utilizing the Mediator pattern, components are decoupled and unaware of each other, reducing direct dependencies. This enhances flexibility and modularity, simplifying the addition or modification of components without impacting the entire system.
Now, let’s delve into how MediatR empowers the realization of these principles.
The MediatR Library
MediatR is a popular open-source library for implementing the Mediator pattern in .NET applications. It provides a simple and elegant way to handle communication between different parts of an application by encapsulating the logic of sending and handling messages.
There are two primary methods for sending messages: Publish and Send. Understanding the differences and trade-offs between these methods is crucial. This understanding will greatly assist you in making informed design decisions for your application.
Configuring an ASP.NET Core API with MediatR
To begin, let’s install the MediatR package: dotnet add package MediatR
Next, in the Program.cs
file, we are going to register MediatR:
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
To ensure the effective usage of MediatR, it is crucial to set up its configuration properly.
After successful installation and configuration, we can proceed with creating a new controller responsible for sending messages with MediatR:
[ApiController] [Route("api/users")] public class UserController : Controller { private readonly IMediator _mediator; public UserController(IMediator mediator) { _mediator = mediator; } }
In the UserController
, we are going to handle user-related operations within the API
. We utilize the IMediator
interface, which we inject through the constructor injection. This injection enables the controller to access MediatR functionality for effectively handling messages and requests.
Besides using the IMediator
interface, starting from the MediatR library version 9.0 this interface has been split into two separate interfaces ISender
and IPublisher
. The ISender
provides the Send()
method capability and IPublisher
the Publish()
method capability. If you want to know more about this, you can find that in one of our articles about MediatR and CQRS.
The Send Method in MediatR
The Send()
method in MediatR facilitates efficient communication between components by handling commands and queries. It is key in promoting decoupling and separation of concerns within the application, resulting in enhanced flexibility and maintainability.
To initiate the process, we first need to create a command class:
public class CreateUserCommand : IRequest<int> { public string UserName { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; }
The CreateUserCommandHandler
Let’s explore the class responsible for creating a new user and returning its ID. This handler class is crucial for the user creation process:
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, int> { private readonly IUserRepository _userRepository; public CreateUserCommandHandler(IUserRepository userRepository) => _userRepository = userRepository; public async Task<int> Handle(CreateUserCommand request, CancellationToken cancellationToken) { var user = new User { UserName = request.UserName, Email = request.Email }; return await _userRepository.CreateUserAsync(user); } }
We have a class name CreateUserCommandHandler
that implements the IRequestHandler<CreateUserCommand, int>
interface. This handler is responsible for processing the CreateUserCommand
by creating a new user with the provided username
and email
. The IUserRepository
dependency is injected into the handler’s constructor. The Handle()
method asynchronously creates the user using the repository and returns the ID of the newly created user.
Invoking and Verifying Our Request
To invoke our request, we can add the CreateUser()
action to our UserController
:
[HttpPost] [ProducesResponseType(StatusCodes.Status201Created)] public async Task<ActionResult<int>> CreateUser(CreateUserCommand command) { int userId = await _mediator.Send(command); return CreatedAtAction(nameof(CreateUser), userId); }
This action expects a CreateUserCommand
parameter and sends it to MediatR using the _mediator.Send()
method. Upon invoking the request, it retrieves the result userId
and includes it in the CreatedAtAction
response. This response serves as the identifier for the newly created user.
Once we send a request to this endpoint, we can verify that MediatR is working as expected:
The Publish Method in MediatR
The Publish()
method in MediatR serves as a one-way communication mechanism, allowing for the broadcast of messages to multiple subscribers within the application. Its primary function is to facilitate the distribution of messages without expecting any response or results in return.
Let’s create an event that notifies about created users:
public class UserCreatedEvent : INotification { public int UserId { get; set; } public string UserName { get; set; } = string.Empty; }
The UserCreatedEvent
class holds a UserId
and the corresponding UserName
.
The UserCreatedEventHandler
Next, let’s define the UserCreatedEventHandler
class for handling the UserCreatedEvent
. It implements the INotificationHandler
interface and receives the UserCreatedEvent
as a parameter:
public class UserCreatedEventHandler : INotificationHandler<UserCreatedEvent> { private readonly IUserRepository _userRepository; private readonly INotificationService _notificationService; public UserCreatedEventHandler(IUserRepository userRepository, INotificationService notificationService) { _userRepository = userRepository; _notificationService = notificationService; } public async Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken) { var admin = await _userRepository.GetAdminUserAsync(); string message = $"New user created: ID: {notification.UserId}, UserName: {notification.UserName}"; await _notificationService.SendNotificationAsync(admin.UserId, message); } }
The Handle()
method retrieves the admin user from the repository and creates a notification message with the ID and username of the newly created user.
This event handler is essential for managing user creation events and executing additional actions or notifications based on these events.
Invoking and Verifying our Request
To invoke our request, we can add the NotifyUser()
action to our UserController
:
[HttpPost("notify")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<IActionResult> NotifyUser(UserCreatedEvent notification) { await _mediator.Publish(notification); return Ok(); }
The NotifyUser()
action accepts a UserCreatedEvent notification
parameter. Using the _mediator.Publish()
method, we publish the notification by executing our handler. Finally, we return an HTTP 200 OK
status code.
Next, let’s open Postman and send a new request to our endpoint notify
:
Conclusion
Throughout this article, we explored the usage of MediatR for command and event handling. We examined the differences between the Publish and Send methods and discussed their specific use cases.
The Publish method enables one-to-many communication by broadcasting messages without expecting a response. It promotes loose coupling, and dynamic subscriptions, and is well-suited for event-driven architectures.
On the flip side, the Send method facilitates one-to-one communication with an expected response. This can be used for requests or commands which expect an immediate response.