Implementando o Padrão CQRS e MediatoR

Introdução

O padrão CQRS (Command Query Responsibility Segregation) é uma abordagem de design que separa as operações de leitura e escrita em diferentes modelos. Essa separação permite otimizar o desempenho, a escalabilidade e a segurança da aplicação. O MediatR é uma biblioteca .NET que facilita a implementação desse padrão, permitindo um design mais limpo e desacoplado. Neste artigo, vamos explorar a implementação do padrão CQRS com o MediatR em uma aplicação ASP.NET Core.

Instalação do MediatR

Para começar, instale o pacote NuGet MediatR.Extensions.Microsoft.DependencyInjection no projeto Application. Este pacote permite integrar o MediatR com o ASP.NET Core, facilitando a injeção de dependências e o uso dos handlers.

    dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

Configurando o MediatR nos Controllers

O MediatR pode ser consumido diretamente nos Controllers ou definido em um Controller base herdado pelos demais. Veja abaixo a implementação em ActivitiesController.

Exemplo de Instância em ActivitiesController

public class ActivitiesController : BaseApiController
{
    private readonly IMediator _mediator;

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

    [HttpGet]
    public async Task<ActionResult<List<Activity>>> GetActivities() 
    {
        return await _mediator.Send(new List.Query());
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Activity>> GetActivity(Guid id) 
    {
        return Ok();
    }
}

Implementação Utilizando um Controller Base

Para evitar a repetição de código, você pode definir um ControllerBase que disponibiliza o MediatR para todos os controllers que o herdam.

using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;

namespace API.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class BaseApiController : ControllerBase
    {
        private IMediator _mediator;
        protected IMediator Mediator => _mediator ??= HttpContext.RequestServices.GetService<IMediator>();
    }
}

Atualizando o ActivitiesController

Com a configuração acima, o ActivitiesController pode ser atualizado para utilizar o Mediator disponibilizado pelo ControllerBase.

public class ActivitiesController : BaseApiController
{
    [HttpGet]
    public async Task<ActionResult<List<Activity>>> GetActivities() 
    {
        return await Mediator.Send(new List.Query());
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Activity>> GetActivity(Guid id) 
    {
        return await Mediator.Send(new Details.Query { Id = id });
    }
}

Implementação de Handlers

Classe Details

A classe Details é responsável por buscar uma atividade específica pelo seu ID.

namespace Application.Activities
{
    public class Details
    {
        public class Query : IRequest<Activity>
        {
            public Guid Id { get; set; }
        }

        public class Handler : IRequestHandler<Query, Activity>
        {
            private readonly DataContext _context;

            public Handler(DataContext context)
            {
                _context = context;
            }

            public async Task<Activity> Handle(Query request, CancellationToken cancellationToken)
            {
                return await _context.Activities.FindAsync(request.Id);
            }
        }
    }
}

Atualizando o Método GetActivity

Atualize o método GetActivity no ActivitiesController para utilizar o handler Details.

[HttpGet("{id}")]
public async Task<ActionResult<Activity>> GetActivity(Guid id) 
{
    return await Mediator.Send(new Details.Query { Id = id });
}

Classe Create

A classe Create permite a criação de novas atividades.

namespace Application.Activities
{
    public class Create
    {
        public class Command : IRequest
        {
            public Activity Activity { get; set; }
        }

        public class Handler : IRequestHandler<Command>
        {
            private readonly DataContext _context;

            public Handler(DataContext context)
            {
                _context = context;
            }

            public async Task<Unit> Handle(Command request, CancellationToken cancellationToken)
            {
                _context.Activities.Add(request.Activity);
                
                await _context.SaveChangesAsync();

                return Unit.Value;
            }
        }
    }
}

Adicionando o Método CreateActivity

Adicione o método CreateActivity no ActivitiesController.

[HttpPost]
public async Task<IActionResult> CreateActivity(Activity activity) 
{
    return Ok(await Mediator.Send(new Create.Command { Activity = activity }));
}

Classe Edit

A classe Edit permite a edição de atividades existentes.

namespace Application.Activities
{
    public class Edit
    {
        public class Command : IRequest
        {
            public Activity Activity { get; set; }
        }

        public class Handler : IRequestHandler<Command>
        {
            private readonly DataContext _context;

            public Handler(DataContext context) 
            {
                _context = context;
            }

            public async Task<Unit> Handle(Command request, CancellationToken cancellationToken)
            {
                var activity = await _context.Activities.FindAsync(request.Activity.Id);

                activity.Title = request.Activity.Title ?? activity.Title;

                await _context.SaveChangesAsync();

                return Unit.Value;
            }
        }
    }
}

Adicionando o Método EditActivity

Adicione o método EditActivity no ActivitiesController.

[HttpPut("{id}")]
public async Task<IActionResult> EditActivity(Guid id, Activity activity)
{
    activity.Id = id;
    return Ok(await Mediator.Send(new Edit.Command { Activity = activity }));
}

AutoMapper

O AutoMapper é uma ferramenta que facilita o mapeamento de propriedades entre objetos. Instale o pacote AutoMapper.Extensions.Microsoft.DependencyInjection no projeto Application.

    dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

Configurando o AutoMapper

Configure o AutoMapper no método ConfigureServices da classe Startup.cs.

    services.AddAutoMapper(typeof(MappingProfiles).Assembly);

Utilizando o AutoMapper

Utilize o AutoMapper nas classes do projeto Application.Activities para mapear as propriedades de Activity enviadas com o Command request.

public class Handler : IRequestHandler<Command>
{
    private readonly DataContext _context;
    private readonly IMapper _mapper;

    public Handler(DataContext context, IMapper mapper) 
    {
        _context = context;
        _mapper = mapper;
    }

    public async Task<Unit> Handle(Command request, CancellationToken cancellationToken)
    {
        var activity = await _context.Activities.FindAsync(request.Activity.Id);

        _mapper.Map(request.Activity, activity);

        await _context.SaveChangesAsync();

        return Unit.Value;
    }
}

CancellationToken

O CancellationToken é usado para cancelar uma solicitação. Envie o CancellationToken como parâmetro para o método ToListAsync do Entity Framework.

Considere a implementação abaixo na classe List.cs na pasta Activities.

public async Task<List<Activity>> Handle(Query request, CancellationToken cancellationToken)
{
    try
    {
        for (var i = 0; i < 10; i++) 
        {
            cancellationToken.ThrowIfCancellationRequested();
            await Task.Delay(1000, cancellationToken);
        }
    } 
    catch (Exception ex) when (ex is TaskCancellationException)
    {
        _logger.LogInformation($"Erro: {ex}");
    }

    return await _context.Activities.ToListAsync();
}

Adicionando CancellationToken no Controller

No controller, receba o CancellationToken e envie-o para o método que executará o processamento.

[HttpGet]
public async Task<ActionResult<List<Activity>>> GetActivities(CancellationToken ct) 
{
    return await Mediator.Send(new List.Query(), ct);
}

Conclusão

Implementar o padrão CQRS com o MediatR em uma aplicação ASP.NET Core proporciona vários benefícios, incluindo uma arquitetura mais limpa, desacoplada e fácil de manter. Utilizar ferramentas como AutoMapper para mapeamento automático de propriedades e CancellationToken para gerenciamento de cancelamentos de tarefas aprimora ainda mais a robustez e a escalabilidade da aplicação. Adotar boas práticas, como a separação clara de responsabilidades e o uso de injeção de dependências, contribui significativamente para a qualidade e a eficiência do desenvolvimento de software.

category:

Arquitetura,Desenvolvimento de Software

Tags:

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *