.NET
- Usando a abordagem DDD : Padrões e práticas - III
![]() |
Hoje veremos como podemos usar a abordagem Domain Driven Design na plataforma .NET apresentando os padrões e práticas relacionadas. |
Application : Casos de uso , CQRS e MediatR
A camada Application na abordagem DDD serve como ponte entre a camada de domínio e as camadas externas do sistema, como a interface do usuário ou serviços externos. Ela é responsável por implementar casos de uso, coordenar tarefas e lidar com questões transversais como validação e autorização. Neste artigo, discutiremos como implementar a camada Application abordaremos conceitos-chave como comandos, consultas, manipuladores e o padrão mediador.
Na abordagem DDD, a camada Application ou camada de aplicação é responsável pelas seguintes tarefas:
Os casos de uso na
camada de aplicação podem ser implementados como comandos
e consultas. Os comandos representam ações que modificam o estado do
sistema, enquanto as consultas recuperam dados sem causar efeitos colaterais.
Na plataforma .NET, Comandos e Consultas podem ser
implementados como classes, com classes Handler correspondentes responsáveis por
executar o comportamento desejado. Os manipuladores interagem com objetos e
serviços de domínio, bem como com componentes de infraestrutura, como
repositórios, para atender ao caso de uso.
Exemplo de Comando:
// Command
public class UpdateCustomerEmailCommand : IRequest
{
public Guid CustomerId { get; set; }
public string NewEmail { get; set; }
}
// Handler
public class UpdateCustomerEmailHandler : IRequestHandler<UpdateCustomerEmailCommand>
{
private readonly ICustomerRepository _customerRepository;
public UpdateCustomerEmailHandler(ICustomerRepository customerRepository)
{
_customerRepository = customerRepository;
}
public async Task<Unit> Handle(UpdateCustomerEmailCommand request, CancellationToken cancellationToken)
{
var customer = await _customerRepository.GetByIdAsync(request.CustomerId);
if (customer == null)
{
throw new NotFoundException("Customer not found.");
}
customer.UpdateEmail(request.NewEmail);
await _customerRepository.UpdateAsync(customer);
return Unit.Value;
}
}
|
Exemplo de consulta:
// Classe de Consulta para obter informações do cliente
public class GetCustomerQuery
{
public int CustomerId { get; set; }
}
// Manipulador (Handler) para a consulta
public class GetCustomerQueryHandler
{
private readonly ICustomerRepository _customerRepository;
public GetCustomerQueryHandler(ICustomerRepository customerRepository)
{
_customerRepository = customerRepository;
}
public Customer Handle(GetCustomerQuery query)
{
// Recupera o cliente pelo Id
return _customerRepository.GetById(query.CustomerId);
}
} |
Preocupações transversais
Preocupações transversais como validação e autorização podem ser tratadas usando
middleware, filtros ou decoradores na camada de aplicação. Por exemplo, a
validação de Comandos e Consultas pode ser realizada utilizando a biblioteca
FluentValidation, que permite aos desenvolvedores
definir regras de validação em uma classe separada, tornando o código mais fácil
de manter e testável.
Como exemplo de uso de Fluent Validation temos a seguir a definição de uma validação para o comando para criar um Customer:
using FluentValidation;
public class CreateCustomerCommandValidator : AbstractValidator<CreateCustomerCommand>
{
public CreateCustomerCommandValidator()
{
RuleFor(command => command.Name)
.NotEmpty().WithMessage("O nome do cliente é obrigatório.")
.MaximumLength(100).WithMessage("O nome do cliente não pode ter mais de 100 caracteres.");
RuleFor(command => command.Email)
.NotEmpty().WithMessage("O email do cliente é obrigatório.")
.MaximumLength(100).WithMessage("O email do cliente não pode ter mais de 100 caracteres.")
.EmailAddress().WithMessage("O email do cliente não é válido.");
}
}
|
Lembrando que para usar a Fluent Validation devemos instalar o respectivo pacote nuget e fazer o registro no container DI:
builder.Services.AddControllers()
.AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Application>());
|
A seguir podemos usar os validadores criados para validar os modelos nas ações dos controladores. Aqui está um exemplo de um controlador que usa a validação:
[ApiController] [Route("api/customers")] public class CustomerController : ControllerBase { private readonly IValidator<Customer> _customerValidator; public CustomerController(IValidator<Customer> customerValidator) { _customerValidator = customerValidator; } [HttpPost] public IActionResult CreateCustomer([FromBody] Customer customer) { var validationResult = _customerValidator.Validate(customer); if (!validationResult.IsValid) { return BadRequest(validationResult.Errors); } // Se a validação passar, continue com a lógica de criação do cliente. // ... } } |
O Padrão Mediador
O Padrão Mediador é um padrão de design comportamental que promove acoplamento
fraco entre objetos centralizando a comunicação entre eles. No contexto da
camada de aplicação, o Padrão Mediador pode ser usado para dissociar a execução
de Comandos e Consultas de sua implementação real.
Uma biblioteca popular que implementa o padrão Mediator é a biblioteca
MediatR
que simplifica o processo de envio e manipulação de comandos e consultas,
permitindo que os desenvolvedores se concentrem na implementação dos casos de
uso sem se preocupar com a fiação entre os diferentes componentes.
A seguir temos um exemplo de como usar o MediatR com o controlador CustomerController:
Primeiro, temos que instalar o pacote NuGet MediatR para configurar a injeção de dependência.
dotnet add package MediatRA seguir temos que configurar o MediatR :
builder.Services.AddMediatR(typeof(Startup));
Agora podemos criar a classe CreateCustomerCommand :
public class CreateCustomerCommand : IRequest<Customer>
{
public string Name { get; set; }
public string Email { get; set; }
}
|
Em seguida, vamos criar um manipulador CreateCustomerCommandHandler para o comando CreateCustomerCommand que será responsável por realizar a lógica de criação do cliente.
public class CreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, Customer>
{
private readonly ICustomerRepository _customerRepository;
public CreateCustomerCommandHandler(ICustomerRepository customerRepository)
{
_customerRepository = customerRepository;
}
public async Task<Customer> Handle(CreateCustomerCommand request, CancellationToken cancellationToken)
{
var newCustomer = new Customer
{
Name = request.Name,
Email = request.Email
};
_customerRepository.Add(newCustomer);
return newCustomer;
}
}
|
Agora vamos criar uma classe de comando que representa a ação de criação do cliente. Vamos chamá-lo de CreateCustomerCommand.
Agora, podemos atualizar o controlador para usar o MediatR e executar o comando:
[ApiController]
[Route("api/customers")]
public class CustomerController : ControllerBase
{
private readonly IMediator _mediator;
public CustomerController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> CreateCustomer([FromBody] CreateCustomerCommand command)
{
var customer = await _mediator.Send(command);
return CreatedAtAction(nameof(GetCustomer), new { customerId = customer.Id }, customer);
}
[HttpGet("{customerId}")]
public IActionResult GetCustomer(int customerId)
{
// Lógica para recuperar um cliente específico
// ...
}
}
|
Aqui, o controlador usa o IMediator para enviar o comando CreateCustomerCommand.
O MediatR cuida de localizar o manipulador apropriado (no caso, CreateCustomerCommandHandler) e executar a lógica de criação do cliente. Quando a criação do cliente é bem-sucedida, o controlador retorna um código de status 201 (Created) com um cabeçalho de localização para o recurso recém-criado.
Infrastruture : Persistência e serviços externos
A camada de
infraestrutura é responsável por fornecer serviços técnicos e implementações que
suportam as camadas superiores do sistema, como mecanismos de persistência,
integrações de serviços externos e configuração.
A camada de infraestrutura desempenha um papel crucial na implementação de DDD
realizando as seguintes operações
- Implementar lógica de persistência e acesso a dados para objetos de domínio;
- Integrar com serviços externos, como filas de mensagens e APIs de terceiros;
- Fornecer serviços técnicos e utilitários, como registro, armazenamento em
cache e configuração;
Na plataforma .NET , a persistência pode ser implementada usando ferramentas ORM como Entity Framework Core, que permite aos desenvolvedores trabalhar com objetos de domínio e mapeá-los para o esquema de banco de dados subjacente. O Entity Framework Core fornece um conjunto de abstrações e convenções para implementar repositórios, gerenciar migrações de banco de dados e consultar dados.
Exemplo de classe de contexto que herda de DbContext do EF Core:
public class ApplicationDbContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{ }
}
|
Na camada de infrastructure também são implementados os repositórios cujas interfaces foram definidos no projeto Domain.
Lidando com integração de serviços externos
A integração de serviços externos, como filas de mensagens e APIs de terceiros,
pode ser implementada usando HttpClient ou
bibliotecas de cliente dedicadas. É uma boa prática criar classes de cliente
personalizadas que encapsulam a lógica de comunicação e fornecem uma abstração
limpa para a camada de aplicação interagir com esses serviços.
Exemplo:
ublic interface IExternalServiceClient
{
Task<ExternalServiceData> GetDataAsync();
}
public class ExternalServiceClient : IExternalServiceClient
{
private readonly HttpClient _httpClient;
public ExternalServiceClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<ExternalServiceData> GetDataAsync()
{
var response = await _httpClient.GetAsync("/api/data");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<ExternalServiceData>(content);
}
}
|
Neste exemplo, a classe ExternalServiceClient encapsula o HttpClient e fornece um método simples para recuperar dados de um serviço externo.
Integração de eventos de domínio para escalabilidade
Os eventos de domínio são um mecanismo poderoso em DDD que permite acoplamento
fraco e melhora a escalabilidade do sistema. Eles representam ocorrências
importantes dentro do domínio e são acionados por agregados quando ocorre uma
mudança de estado.
Benefícios do uso de eventos de domínio
Os eventos de domínio oferecem diversas vantagens em uma implementação DDD como
:
- Promover o acoplamento fraco, permitindo que os
componentes reajam aos eventos sem estarem diretamente vinculados à fonte
- Melhorar a escalabilidade permitindo que os eventos sejam processados de forma
assíncrona ou em paralelo
- Encapsular preocupações transversais e efeitos colaterais, como envio de
notificações ou atualização de sistemas externos
Existem diferentes
abordagens para implementar o envio e tratamento de eventos de domínio, como:
Em processo: Os eventos são despachados e
tratados dentro do mesmo processo, normalmente usando um mediador ou um
barramento de eventos. Essa abordagem é mais simples e fornece menor latência,
mas pode ser menos escalonável.
Fora do processo: os eventos são enviados
para corretores de mensagens externos ou arquiteturas orientadas a eventos, como
Apache Kafka ou Azure Event Grid. Essa abordagem oferece melhor escalabilidade e
tolerância a falhas, mas introduz complexidade e latência adicionais.
A implementação de eventos de domínio em C# envolve a criação de classes de eventos, editores, assinantes e manipuladores. Os exemplos de código a seguir demonstram como implementar um mecanismo de envio e manipulação de eventos de domínio em processo.
public interface IDomainEvent
{
DateTime OccurredOn { get; }
}
public class CustomerEmailChangedEvent : IDomainEvent
{
public Customer Customer { get; }
public string OldEmail { get; }
public DateTime OccurredOn { get; }
public CustomerEmailChangedEvent(Customer customer, string oldEmail)
{
Customer = customer;
OldEmail = oldEmail;
OccurredOn = DateTime.UtcNow;
}
}
|
Implementando um evento publicador :
public interface IDomainEventPublisher
{
Task PublishAsync<TEvent>(TEvent domainEvent) where TEvent : IDomainEvent;
}
public class InProcessDomainEventPublisher : IDomainEventPublisher
{
private readonly IServiceProvider _serviceProvider;
public InProcessDomainEventPublisher(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task PublishAsync<TEvent>(TEvent domainEvent) where TEvent : IDomainEvent
{
var handlers = _serviceProvider.GetServices<IDomainEventHandler<TEvent>>();
foreach (var handler in handlers)
{
await handler.HandleAsync(domainEvent);
}
}
|
Implementando um manipulador de evento de dominio:
public interface IDomainEventHandler<in TEvent> where TEvent : IDomainEvent
{
Task HandleAsync(TEvent domainEvent);
}
public class CustomerEmailChangedEventHandler : IDomainEventHandler<CustomerEmailChangedEvent>
{
public Task HandleAsync(CustomerEmailChangedEvent domainEvent)
{
// Handle the event, e.g., send a notification or update an external system
Console.WriteLine($"Customer email changed from {domainEvent.OldEmail} to {domainEvent.Customer.Email}");
return Task.CompletedTask;
}
}
|
Disparando o evento e manipulando :
// Atualiza o email e dispara o evento no domain
customer.UpdateEmail(newEmail);
await _domainEventPublisher.PublishAsync(new CustomerEmailChangedEvent(customer, oldEmail));
// Registra o event publisher e handler no containeir DI
services.AddSingleton<IDomainEventPublisher, InProcessDomainEventPublisher>();
services.AddTransient<IDomainEventHandler<CustomerEmailChangedEvent>, CustomerEmailChangedEventHandler>();
|
Neste exemplo, o CustomerEmailChangedEvent é acionado quando o e-mail de um cliente é atualizado e o CustomerEmailChangedEventHandler trata o evento enviando uma notificação ou atualizando um sistema externo. O editor e o manipulador de eventos são registrados no contêiner de injeção de dependência para garantir que sejam resolvidos adequadamente em tempo de execução.
E estamos
conversados...
"E Jesus lhe disse: Vai, a tua fé te salvou. E logo
viu, e seguiu a Jesus pelo caminho."
Marcos 10:52
Referências:
C# - Tasks x Threads. Qual a diferença
DateTime - Macoratti.net
Null o que é isso ? - Macoratti.net
Formatação de data e hora para uma cultura ...
C# - Calculando a diferença entre duas datas
NET - Padrão de Projeto - Null Object Pattern
C# - Fundamentos : Definindo DateTime como Null ...
C# - Os tipos Nullable (Tipos Anuláveis)