É muito comum encontrar nos sistemas corporativos, o lançamento de Exceptions ao realizar validações de regras de negócio. Afinal de contas, é necessário informar ao usuário que algo deu errado.
public class Customer
{
public string Name { get; }
public string Email { get; }
public Customer(string name, string email)
{
if (string.IsNullOrEmpty(name))
{
throw new InvalidOperationException("Name cannot be empty");
}
if (!email.Contains("@"))
{
throw new InvalidOperationException("Invalid email");
}
Name = name;
Email = email;
}
}
Porém, o que muitos desenvolvedores esquecem, é que exceptions são inesperadas e elas indicam uma falha – uma exceção ao funcionamento normal do sistema.
Uma validação de regra de negócio é esperada e normal em praticamente todos os sistemas. Acontecem a todo momento e não são falhas para serem tratadas como exceptions.
O lançamento de exceptions no domínio traz alguns problemas:
- Interrupção do fluxo de execução para cada inconsistência encontrada durante as verificações de regras de negócio
- São onerosas para o processador
- São deselegantes
Uma abordagem superior seria usar Notifications!
Você pode conferir os exemplos de código em meu GitHub.
Notification Pattern
Para capturar as mensagens das validações de domínio, podemos usar o pattern Notification ou Domain Notification, como também é conhecido, que foi descrito por Martin Fowler, em um artigo de 2004, que também mostra um modelo de implementação em C#.
Basicamente, esse pattern nos ajuda a levar mensagens de domínio para a camada de apresentação como, por exemplo, erros ou mensagens de validação de negócio, já que normalmente a camada de apresentação não possui nenhum acesso direto à camada de domínio.
Existem variações ao desenho de camadas, como mostrado acima. Eu mesmo uso modelos diferentes, dependendo da aplicação. Mas, normalmente, em nenhuma delas a camada de apresentação acessa o domínio diretamente. Se a sua aplicação faz isso, reveja seu desenho arquitetural.
Implementando Notification Pattern no ASP.Net Core
Existem variações em sua implementação, mas, basicamente, uma notificação pode ser representada como um objeto que encapsula uma mensagem gerada pelo domínio, e pode também ser representada como algo mais simples – uma coleção de strings, por exemplo.
Em nosso cenário, representaremos as notificações como uma coleção de Notification na classe NotificationContext, conforme veremos a seguir.
public class Notification
{
public string Key { get; }
public string Message { get; }
public Notification(string key, string message)
{
Key = key;
Message = message;
}
}
public class NotificationContext
{
private readonly List<Notification> _notifications;
public IReadOnlyCollection<Notification> Notifications => _notifications;
public bool HasNotifications => _notifications.Any();
public NotificationContext()
{
_notifications = new List<Notification>();
}
public void AddNotification(string key, string message)
{
_notifications.Add(new Notification(key, message));
}
public void AddNotification(Notification notification)
{
_notifications.Add(notification);
}
public void AddNotifications(IReadOnlyCollection<Notification> notifications)
{
_notifications.AddRange(notifications);
}
public void AddNotifications(IList<Notification> notifications)
{
_notifications.AddRange(notifications);
}
public void AddNotifications(ICollection<Notification> notifications)
{
_notifications.AddRange(notifications);
}
public void AddNotifications(ValidationResult validationResult)
{
foreach (var error in validationResult.Errors)
{
AddNotification(error.ErrorCode, error.ErrorMessage);
}
}
}
Além da classe Notification, que é uma estrutura bem simples e serve apenas para representar uma mensagem, também temos a classe NotificationContext, que é responsável por armazenar as notificações através da propriedade Notifications.
Temos, também, a propriedade HasNotifications, que serve apenas para informar se existem notificações no contexto e alguns métodos auxiliares que servem para adicionar mensagens ao contexto de notificações.
No domínio temos uma classe base chamada Entity, que possui um método de validação genérico e algumas propriedades que indicam o estado da entidade.
public abstract class Entity
{
public Guid Id { get; protected set; }
public bool Valid { get; private set; }
public bool Invalid => !Valid;
public ValidationResult ValidationResult { get; private set; }
public bool Validate<TModel>(TModel model, AbstractValidator<TModel> validator)
{
ValidationResult = validator.Validate(model);
return Valid = ValidationResult.IsValid;
}
}
public class Customer : Entity
{
public string Name { get; }
public string Email { get; }
public Customer(string name, string email)
{
Id = Guid.NewGuid();
Name = name;
Email = email;
Validate(this, new CustomerValidator());
}
}
public class CustomerValidator : AbstractValidator<Customer>
{
public CustomerValidator()
{
RuleFor(a => a.Email)
.NotEmpty()
.EmailAddress()
.WithMessage("Invalid email");
RuleFor(a => a.Name)
.NotEmpty()
.WithMessage("Name cannot be empty");
}
}
Para exemplificar, criei uma entidade Customer, que recebe alguns parâmetros em seu construtor.
Como já sabemos, em um domínio rico, as próprias entidades possuem a responsabilidade de se validarem. Então, no construtor, o método Validate da classe base é chamado onde passamos como parâmetro uma instância da própria entidade e sua definição de validações, feita com o FluentValidation.
Essa entidade expõe as mensagens de erro das validações através da propriedade ValidationResult da classe base.
Nesse exemplo de domínio eu estou usando a biblioteca FluentValidation para fazer as validações, mas você pode usar o que achar melhor.
Geralmente, também criamos um ApplicationService, que será responsável por orquestrar as interações entre a apresentação, domínio e repositório, mas como vocês já sabem, eu gosto de usar o MediatR em meus projetos e acabo não criando classes de serviço na camada de Application, conforme já falei em um artigo anterior.
Nesse caso, os ApplicationServices são substituídos por Handlers do MediatR. Com isso, recebemos um request/command e fazemos toda a orquestração para um “use case” específico.
public class CreateCustomerHandler : IRequestHandler<CreateCustomer, Guid>
{
private readonly NotificationContext _notificationContext;
private readonly ICustomerRepository _customerRepository;
public CreateCustomerHandler(
NotificationContext notificationContext,
ICustomerRepository customerRepository)
{
_notificationContext = notificationContext;
_customerRepository = customerRepository;
}
public async Task<Guid> Handle(CreateCustomer request, CancellationToken cancellationToken)
{
var customer = new Customer(request.Name, request.Email);
if (customer.Invalid)
{
_notificationContext.AddNotifications(customer.ValidationResult);
return Guid.Empty;
}
await _customerRepository.Save(customer);
return customer.Id;
}
}
No método Handle, após instanciar a entidade Customer, ela armazenará o resultado de suas validações na propriedade ValidationResult.
Ao verificar que a entidade está inválida, podemos adicionar suas mensagens de validação no contexto de notificações que foi injetado no construtor do Handler.
Isso nos permite acumular as falhas e exibir todas ao mesmo tempo para o usuário. Com a entidade inválida não faz sentido avançar, então podemos interromper o fluxo de execução com um simples return.
Perceba que interrompemos o fluxo de execução apenas uma vez – após todas a validações terem sido feitas e apenas para notificar as mensagens ao usuário.
Ao fazer essa interrupção, o fluxo volta para a controller que originou o request para o Handler.
[Route("api/[controller]")]
[ApiController]
public class CustomersController : ControllerBase
{
private readonly IMediator _mediator;
public CustomersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] CreateCustomer command)
{
var id = await _mediator.Send(command);
return Created($"api/customers/{id}", id);
}
}
Veja que a action está limpa – ela só envia o request para o MediatR e devolve o resultado para o client. Simples assim!
Mas e as notificações?
Elas são capturadas por um filtro global, chamado NotificationFilter, o qual implementa a interface IAsyncResultFilter. Isso quer dizer que esse filtro será invocado automaticamente, após a action da controller gerar um resultado válido, e antes do retorno para o client. Com isso, podemos interceptar o resultado da action e formatá-lo conforme necessário.
Para mais informações sobre como os filtros funcionam, veja a documentação oficial.
public class NotificationFilter : IAsyncResultFilter
{
private readonly NotificationContext _notificationContext;
public NotificationFilter(NotificationContext notificationContext)
{
_notificationContext = notificationContext;
}
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
if (_notificationContext.HasNotifications)
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
context.HttpContext.Response.ContentType = "application/json";
var notifications = JsonConvert.SerializeObject(_notificationContext.Notifications);
await context.HttpContext.Response.WriteAsync(notifications);
return;
}
await next();
}
}
Basicamente verificamos se existem notificações através da propriedade HasNotifications do NotificationContext. Se existirem mensagens, mudamos o Http Status Code para Bad Request (400) e retornamos a lista de notificações em formato Json.
Se você não quiser usar filtros, pode criar um objeto que encapsula os resultados da sua camada Application, adicionando as mensagens de notificação ou o resultado da operação nele. Entretanto, será necessário verificar se existem notificações na controller e gerar o Bad Request em cada action.
Testes
Ao tentar fazer uma chamada para a nossa API com os dados do Customer inválidos, as mensagens de validação são exibidas todas de uma vez.
Conclusão
O uso de padrões como Domain Notifications traz uma certa flexibilidade aos nossos sistemas e ajuda a manter nosso código mais simples e elegante, além de não impactar a performance da aplicação com o lançamento de exceptions desnecessárias.
Existem diversas formas de implementar o Notification Pattern, sendo a que vimos aqui uma delas. Outra implementação que gosto bastante é o Flunt, uma biblioteca idelizada e mantida pelo Microsoft MVP, André Baltieri, em conjunto com a comunidade.
Espero que tenham gostado desse artigo. Deixem seus comentários, dúvidas ou sugestões logo abaixo.
Abraços!