El Poder del Patrón de Mediador en .NET con MediatR

Hablemos hoy de la librería MediatR, la que en teoría nos debe ayudar a hacer nuestras aplicaciones más limpias y desacopladas, ya que claro utiliza el patrón mediador. 

 

 

 

1 - Qué es MediatR?

Mediatr no es más que una librería que implementa el patrón mediador en C#, el cual nos permite comunicar objetos o clases de una forma indirecta. Por lo tanto para entender MediatR debemos entender que es el patrón mediador.

 

 

 

1.1 - Qué es el patrón mediador? 

El patrón mediador es un patrón de diseño que se utiliza para reducir la complejidad entre la comunicación de múltiples clases. Lo que hace es proporcionarnos un mediador que se encarga del control e interacción de los objetos.

 

Básicamente la librería MediatR es ese mediador, lo que hacemos es en vez de Clase 1 llama a clase 2, MediatR se encarga de saber, utilizando handlers y tipos donde tiene que ir la llamada.

 

Para verlo un poco mejor veamos el siguiente ejemplo, que siempre es mas fácil con imágenes.

caso de uso sin mediatr

Básicamente en la imagen podemos ver un endpoint que simula el actualizar un ítem en una tienda, lo que simula es que vamos a cambiar el precio, obviamente nuestro caso de uso tiene una serie de reglas que debemos mantener las cuales son las validaciones, guardamos en la base de datos, y es posible que necesitemos invocar otros casos de uso, por ejemplo, si el precio baja más de un 30%, todos los usuarios que tengan ese ítem en la lista de deseos, recibirán un email. Este caso de uso puede llamar a más casos de uso, etc. así hasta el infinito.

 

Esta estructura hace que nuestra aplicación  esté muy acoplada (coupled), en otras palabras, que se hacen llamadas a todas partes y está muy chapucero todo. 

 

Aquí es donde entra el patrón mediador (mediator pattern), básicamente nos da este objeto que hace de mediador el que se encarga de juntarlo todo, pero a la vez, todo está separado. 

ejemplo con mediatr

Como estamos viendo en la imagen, TODO llama y es llamado desde nuestro mediador, incluido el endpoint.

Además en los casos de uso tenemos un cambio, ya no estamos llamando directamente a los casos de uso, sino que estamos notificando que algo ha sucedido, y será el mediador el que se encargue de notificar a quien tenga que ser notificado. 

 

Aquí es donde entra MediatR, en C#, MediatR es la librería que se encarga de toda esta comunicació, saber qué servicio tiene que invocarse, etc. 

 

 

2 - Ventajas de usar el patrón mediador

Las ventajas son claras a simple vista, la principal es el desacoplamiento ya no estamos llamando de una clase a la otra, sino que estamos utilizando el mediador, lo que reduce dependencias y por supuesto ayuda con el mantenimiento y potencialmente con las pruebas unitarias

 

Hay mucha gente que te dira que el patron mediator es esencial para hacer CQRS y que sin MediatR (en .net) no se puede hacer, la verdad es que este punto no lo comparto. Se puede aplicar CQRS sin el patron mediador perfectamente.

 

Y finalmente el tema de los eventos, cuando un caso de uso termina, en verdad en cualquier momento, tenemos (si queremos) un comando de notificación con los cambios, lo que permite ejecutar otros procesos de forma asíncrona sin necesidad de tener tareas en segundo plano corriendo constantemente, y por supuesto sin necesidad de tener un service bus como parte de la infraestructura.

 

 

3 - Implementación del patrón mediador en C# con MediatR

Ahora vamos a pasar a la parte más práctica, y lo veremos con un ejemplo sencillo donde va a quedar claro el uso. Como siempre el código está disponible en GitHub; de hecho, para poder comparar, esta tanto en versión con MediatR como en versión sin MediatR.

 

Lo que vamos a simular es el ejemplo de antes, donde modificamos un elemento, por simplicidad está todo en un único proyecto, y del repositorio, tenemos únicamente la interfaz, la implementación es un Fake, para ahorrar tiempo:

public interface IDatabaseRepository
{
    Task<bool> UpdateItem(int itemId, decimal newPrice, string title);
    Task<ItemDto> GetItemById(int id);
}

/// <summary>
/// This is only to simulate the example.
/// </summary>
public class FakeDatabaseRepository : IDatabaseRepository
{
    public Task<bool> UpdateItem(int itemId, decimal newPrice, string title)
    {
        return Task.FromResult(true);
    }

    public Task<ItemDto> GetItemById(int id)
    {
        return Task.FromResult(new ItemDto(id, 12, "Title string"));
    }
}

Nota: ItemDto es muy simple, únicamente Id, precio y título.

 

3.1  - Implementación sin MediatR

La mayoría de los que estáis aquí es porque no estáis familiarizados con el patrón mediador, así que en mi opinión la forma más fácil de entender lo que estamos haciendo es primero comprendiendo cómo sería sin utilizarlo.

 

Para ello tenemos casos de uso, otras empresas lo pueden llamar services, o como sea, a mi me gusta llamar a las cosas por su nombre; y en nuestro caso tenemos dos, UpdateItem y NotifyWishlist. El código es bastante sencillo, algo así hará el trabajo: 

public class UpdateItem
{
    private readonly IDatabaseRepository _databaseRepository;
    private readonly NotifyWishlist _notifyWishlist;

    public UpdateItem(IDatabaseRepository databaseRepository, NotifyWishlist notifyWishlist)
    {
        _databaseRepository = databaseRepository;
        _notifyWishlist = notifyWishlist;
    }
    public async Task<bool> Execute(ItemDto itemToUpdate)
    {

        if (itemToUpdate.Title.Length > 200)
            throw new Exception("Title must be less than 200 characters");
        if (itemToUpdate.Price <= 0)
            throw new Exception("It can't be free");

        ItemDto existingItem = await _databaseRepository.GetItemById(itemToUpdate.Id);
        await _databaseRepository.UpdateItem(itemToUpdate.Id, itemToUpdate.Price, itemToUpdate.Title);

        decimal percentageDifference = ((itemToUpdate.Price - existingItem.Price) / existingItem.Price) * 100;
        if (percentageDifference <= -30)
        {
           await _notifyWishlist.Execute(itemToUpdate.Id);
        }
            

        return true;
    }
}


public class NotifyWishlist
{
    public Task Execute(int id)
    {
        //We dont need this working for the example.
        Console.WriteLine("Logic to get who is wishlisting that ID and send the emails");
        
        return Task.FromResult(true);
    }
}

Para simplificar, si algo no funciona, lanzo una excepción, pero ya sabeis que yo soy partidario de utilizar Result<T>, aquí te dejo un link con la explicación y la implementación.

 

En este caso, estamos ejecutando el caso de uso de las notificaciones en el mismo proceso que actualizamos el ítem, lo cual no es muy buena práctica. Para evitar esta espera, podríamos tirar por una tarea en segundo plano con hangfire. Pero aquí es donde entra la fuerza de MediatR que veremos en un momento.

 

Y luego el controller no tiene nada en especial, simplemente llamamos al caso de uso: 

[ApiController]
[Route("[controller]")]
public class DefaultExampleController : ControllerBase
{
    public readonly UpdateItem _UpdateItem;

    public DefaultExampleController(UpdateItem updateItem)
    {
        _UpdateItem = updateItem;
    }

    [HttpPut("item")]
    public async Task<bool> UpdateItem(ItemDto itemDto)
        => await _UpdateItem.Execute(itemDto);
}

Esto sería un proceso normal, o por defecto. Ahora vamos a ver como es este ejemplo si implementamos el patrón mediador

 

 

3.2 - Ejemplo Implementando el patrón mediador. 

Ahora pasamos al ejemplo de código utilizando la librería MediatR. Lo primero como siempre, ir a nuget e instalar el paquete MediatR.

 

Una vez instalado,  lo que vamos a hacer es seguir la estructura “recomendada” cuando usamos MediatR.

 

Como he dicho antes, es muy muy normal, separar por Commands and Queries, lo que significa por modificaciones o consultas, si como la respuesta suele llevar el término “response”, y luego cada Query o command necesita un handler, que va a ser el encargado de ejecutar la lógica correspondiente.

 

Lo primero que vamos a hacer es crear un command ya que vamos a ejecutar una modificación, Para ello hacemos lo siguiente: 

public record UpdateItemCommand : IRequest<bool>
{
    public required int Id { get; init; }
    public required decimal Price { get; init; }
    public required string Title { get; init; }
}

Como puedes ver, este command hereda de IRequest<T>, y esto es importante, porque cuando creamos un command, podemos especificarlo con IRequest o IRequest<T>, si indicamos el tipo, lo que estamos haciendo es definir el tipo que va a devolver el handler que vamos a ejecutar. 

 

Ahora simplemente creamos el handler, muy similar al anterior caso de uso:

public class UpdateItemCommandHandler : IRequestHandler<UpdateItemCommand, bool>
{
    private readonly IDatabaseRepository _databaseRepository;
    private readonly IMediator _mediator;

    public UpdateItemCommandHandler(IDatabaseRepository databaseRepository, IMediator mediator)
    {
        _databaseRepository = databaseRepository;
        _mediator = mediator;
    }

    public async Task<bool> Handle(UpdateItemCommand request, CancellationToken cancellationToken)
    {
        if (request.Title.Length > 200)
            throw new Exception("Title must be less than 200 characters");
        if (request.Price <= 0)
            throw new Exception("It can't be free");

        ItemDto existingItem = await _databaseRepository.GetItemById(request.Id);
        await _databaseRepository.UpdateItem(request.Id, request.Price, request.Title);

        await _mediator.Publish(new ItemUpdated()
        {
            Id = request.Id,
            NewPrice = request.Price,
            NewTitle = request.Title,
            OldPrice = existingItem.Price,
            OldTitle = existingItem.Title
        }, cancellationToken);

        return true;
    }
}

Similar pero con alguna diferencia, veamos dichas diferencias en detalle.

  1. De primeras, el handler hereda de IRequestHandler<UpdateItemCommand, bool>, que es el command que hemos creado y el tipo de retorno especificado en el command. Luego la propia interfaz IRequestHandler, es la que utiliza MediatR para identificar que este handler va con el tipo que hemos creado.
  2. Si seguimos hacia abajo, vemos que ya no hemos inyectado nuestro caso de uso de notificar, sino que estamos inyectando la interfaz IMediator, ahora veremos por qué.
  3. El método Handle es la puerta de entrada a nuestro código, y en teoría el único que debería ser público, aquí es donde entra el desacoplamiento mencionado antes, es únicamente este método y esta lógica lo que se va a ejecutar. Si te fijas, este método recibe un parámetro de entrada que es el primer generic de IRequestHandler y el tipo de retorno es el último.
    1. Si nuestro ejemplo fuera IRequest sin tipo genérico, el Handle simplemente no devuelve nada (solo async Task Handle).
  4. La parte de validación en inserción a la base de datos es la misma, aunque verás muchos ejemplos donde la validación se realiza con Fluent Validations, que permite separar, una vez más, la lógica desacoplando el código, o en una pipeline de mediatr que también veremos luego.
  5. Finalmente la parte donde en mi opinión MediatR brilla, ya no estamos llamando al caso de uso de las notificaciones, sino que estamos publicando un evento de dominio. Aquí lo que hace MediatR es actuar como service bus (patrón productor/consumidor) en memoria.

Para publicar estos mensajes tenemos que hacer dos cosas, primero, crear nuestro objeto a publicar, el cual es un objeto normal que hereda de INotification

public record ItemUpdated : INotification
{
    public required int Id { get; init; }
    public required decimal NewPrice { get; init; }
    public required decimal OldPrice { get; init; }
    public required string NewTitle { get; init; }
    public required string OldTitle { get; init; }
}

 

Y después tenemos un handler de la notificación, el cual va a leer dicho evento y procesarlo:

public class ItemUpdatedEventHandler : INotificationHandler<ItemUpdated>
{
    //We dont need this working for the example.
    public async Task Handle(ItemUpdated notification, CancellationToken cancellationToken)
    {
        decimal percentageDifference = ((notification.NewPrice - notification.OldPrice) / notification.NewPrice) * 100;

        if (percentageDifference <= -30)
        {
            Console.WriteLine("Logic to get who is wishlisting that ID and send the emails");
        }
    }
}

Y ya está, la lógica principal de MediatR es esto, lo que queda ahora es incluirlo todo en el contenedor de dependencias, lo que se hace con el método AddMediatR(Asssembly) en :

builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); 
});

 

Y finalmente crear el controlador que va a ejecutar nuestro handler.

[ApiController]
[Route("[controller]")]
public class MediatRExampleController : ControllerBase
{
    private readonly ISender _sender;

    public MediatRExampleController(ISender sender)
    {
        _sender = sender;
    }

    [HttpPut("item")]
    public async Task<bool> UpdateItem(ItemDto itemDto)
        => await _sender.Send(new UpdateItemCommand()
        {
            Id = itemDto.Id,
            Price = itemDto.Price,
            Title = itemDto.Title
        });
}

Aquí podemos ver que estamos inyectando la interfaz ISender, que también es parte de MediatR y el encargado de hacer toda la magia para llamar al handler correspondiente para cada evento cuando es ejecutado.


Para replicar el escenario que teníamos antes, esto es todo lo que debemos hacer. Sencillo y facil de entender, aunque algunos pueden opinar que MediatR necesita mucho código boilerplate, al margen de este detalle, sin problemas. 

 

 

3.2.1 - Pipeline personalizada en MediatR

Este es el ejemplo del código, como vemos muy sencillo utilizando mediatr; Si queremos ir un poco más allá, aunque en este post no lo cubriremos en mucho detalle, podemos ir a las pipelines de MediatR, que no es más que aplicar el patrón decorator en Mediatr.

 

Con esta funcionalidad podemos especificar que los handlers (específicos o todos) tengan una pipeline dentro de MediatR, es decir que algo suceda tanto antes como después.

Aquí vemos un ejemplo:

services.AddMediatR(cfg => {
   cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
   cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>));
   cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(AuthorizationBehaviour<,>));
   cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
   cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(PerformanceBehaviour<,>));
});

Como vemos en este caso la pipeline contiene  4 elementos (además del handler) y cada uno de estos IPipelineBehaviour permite configurar código para ser ejecutado mientras sucede la request o mientras sucede la respuesta. Y se ejecutan en orden.

Básicamente es lo mismo que podemos hacer con middlewares y filtros pero dentro de mediatr directamente, por lo que  actuan de substituto para poner un error handler global, o por ejemplo caché en uno de los handlers, etc.

Nota: en el vídeo iremos mas en detalle sobre este escenario que esta sacado de este repositorio de github.

 

 

4 - El uso de MediatR y el patrón mediador en las empresas

La implementación del patrón mediator (como muchos otros patrones) se ha llevado a un extremo que es absolutamente ridículo, me explico, he mencionado que el mediador es el encargado de administrar todas estas llamadas, lo cual a priori parece muy bueno, pero en la práctica no lo es tanto. 

 

Aunque si bien no es culpa del patrón o de la librería, es del uso que se hace de ella. 

Nos encontramos con proyectos que desde un handler de MediatR están llamando a otro, porque antes de devolver la información al usuario, necesita que lo que sea que otro handler está haciendo, sea ejecutado.

Lo que quiere decir que para la imagen de antes tenemos algo como lo siguiente:

mediator mal implementadoComo puedes ver, tenemos una línea roja que cruza de un caso de uso al otro, ¿te recuerda esto a algo? Sí! A la primera imagen que hemos visto, donde todo estaba entrelazado, pues ahora con esteroides, porque no solo todo está entrelazado, sino que tenemos mediatr por el medio.

 

En muchos casos es nuevo handler que se está llamando, está siendo utilizado únicamente por el handler actual, lo que significa que esa lógica no debería estar en un handler, sino en una clase normal. 

Pero como digo, se lleva al extremo de: "si no esta en un handler, esta mal", y esa suposición es ridícula. 

 

Llevo muchos años trabajando (y en muchas empresas) y eso hace que haya visto mucho en la programación; que un handler llame a otro puede ser un problema, pero lo más bizarro que he visto yo es una abstracción personalizada encima de MediatR, que lo que hace es permitir varios tipos genéricos como parámetros de entrada, obviamente la cosa no acaba ahí, el proyecto era bastante grande y había handlers que inyectaban otros 10 o 12, porque como digo, las implementaciones se llevan a extremos ridículos, todo acaba mal implementado y al final lo que un dia ayudó, ahora solo es un engorro.

 

Otro punto muy importante, que a primera vista puedes pensar que no es un problema muy grande, es que MediatR no facilita la navegación de código, por ejemplo cuando publicas un evento, no puedes hacer F12 para ver donde va ese código, sino que tienes que buscar por texto en el proyecto, o los usos del evento que publicas. Como digo que no es un problema muy grande si el proyecto no es enorme y está bien implementado, pero en casos como el anterior, puedes tirarte horas para encontrar exactamente lo que estás buscando. 

 

Lo dije en un post de twitter, nunca he encontrado un proyecto donde MediatR ayude, al final todos acaban siendo un amasijo de proyectos (csproj) , excesivo uso de interfaces, de SRP, pese a que soy fan, como siempre es llevado a lo absurdo y locuras varias que lo único que hacen es over-engineer un código que debería ser mucho más fácil de leer y entender. 

Utilizar mediatR o el patrón mediador cuando está bien implementado es una maravilla, estas implementaciones se desvían muy rápido del cauce correcto lo que hace que trabajar en proyectos con dicho patrón se vuelva muy difícil así que yo personalmente, pese a que se de sus ventajas, intentó evitar el uso.

 


Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2024 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café