Implementar a camada de aplicativo de microsserviço usando a API da Web
Gorjeta
Este conteúdo é um trecho do eBook, .NET Microservices Architecture for Containerized .NET Applications, disponível no .NET Docs ou como um PDF para download gratuito que pode ser lido offline.
Use a injeção de dependência para injetar objetos de infraestrutura na camada de aplicativo
Como mencionado anteriormente, a camada de aplicativo pode ser implementada como parte do artefato (assembly) que você está criando, como dentro de um projeto de API Web ou um projeto de aplicativo Web MVC. No caso de um microsserviço criado com ASP.NET Core, a camada de aplicativo geralmente será sua biblioteca de API da Web. Se você quiser separar o que está vindo do ASP.NET Core (sua infraestrutura mais seus controladores) do código da camada de aplicativo personalizada, você também pode colocar sua camada de aplicativo em uma biblioteca de classes separada, mas isso é opcional.
Por exemplo, o código da camada de aplicativo do microsserviço de pedido é implementado diretamente como parte do projeto Ordering.API (um projeto ASP.NET Core Web API), como mostra a Figura 7-23.
A exibição Gerenciador de Soluções do microsserviço Ordering.API, mostrando as subpastas na pasta Aplicativo: Comportamentos, Comandos, DomainEventHandlers, IntegrationEvents, Modelos, Consultas e Validações.
Figura 7-23. A camada de aplicativo no projeto Ordering.API ASP.NET Core Web API
ASP.NET Core inclui um contêiner IoC integrado simples (representado pela interface IServiceProvider) que suporta injeção de construtor por padrão e ASP.NET disponibiliza determinados serviços por meio de DI. ASP.NET Core usa o termo serviço para qualquer um dos tipos que você registrar que serão injetados através do DI. Você configura os serviços do contêiner interno no arquivo de Program.cs do seu aplicativo. Suas dependências são implementadas nos serviços que um tipo precisa e que você registra no contêiner IoC.
Normalmente, você deseja injetar dependências que implementam objetos de infraestrutura. Uma dependência típica para injetar é um repositório. Mas você pode injetar qualquer outra dependência de infraestrutura que possa ter. Para implementações mais simples, você pode injetar diretamente seu objeto de padrão Unit of Work (o objeto EF DbContext), porque o DBContext também é a implementação de seus objetos de persistência de infraestrutura.
No exemplo a seguir, você pode ver como o .NET está injetando os objetos de repositório necessários por meio do construtor. A classe é um manipulador de comando, que será abordado na próxima seção.
public class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, bool>
{
private readonly IOrderRepository _orderRepository;
private readonly IIdentityService _identityService;
private readonly IMediator _mediator;
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
private readonly ILogger<CreateOrderCommandHandler> _logger;
// Using DI to inject infrastructure persistence Repositories
public CreateOrderCommandHandler(IMediator mediator,
IOrderingIntegrationEventService orderingIntegrationEventService,
IOrderRepository orderRepository,
IIdentityService identityService,
ILogger<CreateOrderCommandHandler> logger)
{
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
_identityService = identityService ?? throw new ArgumentNullException(nameof(identityService));
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<bool> Handle(CreateOrderCommand message, CancellationToken cancellationToken)
{
// Add Integration event to clean the basket
var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(message.UserId);
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStartedIntegrationEvent);
// Add/Update the Buyer AggregateRoot
// DDD patterns comment: Add child entities and value-objects through the Order Aggregate-Root
// methods and constructor so validations, invariants and business logic
// make sure that consistency is preserved across the whole aggregate
var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode);
var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration);
foreach (var item in message.OrderItems)
{
order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Units);
}
_logger.LogInformation("----- Creating Order - Order: {@Order}", order);
_orderRepository.Add(order);
return await _orderRepository.UnitOfWork
.SaveEntitiesAsync(cancellationToken);
}
}
A classe usa os repositórios injetados para executar a transação e persistir as alterações de estado. Não importa se essa classe é um manipulador de comandos, um método de controlador de API Web Core ASP.NET ou um Serviço de Aplicativo DDD. Em última análise, é uma classe simples que usa repositórios, entidades de domínio e outras coordenações de aplicativos de forma semelhante a um manipulador de comandos. Dependency Injection funciona da mesma maneira para todas as classes mencionadas, como no exemplo usando DI com base no construtor.
Registrar os tipos de implementação de dependência e interfaces ou abstrações
Antes de usar os objetos injetados através de construtores, você precisa saber onde registrar as interfaces e classes que produzem os objetos injetados em suas classes de aplicativo por meio de DI. (Como DI com base no construtor, como mostrado anteriormente.)
Use o contêiner IoC integrado fornecido pelo ASP.NET Core
Ao usar o contêiner IoC interno fornecido pelo ASP.NET Core, você registra os tipos que deseja injetar no arquivo Program.cs , como no código a seguir:
// Register out-of-the-box framework services.
builder.Services.AddDbContext<CatalogContext>(c =>
c.UseSqlServer(Configuration["ConnectionString"]),
ServiceLifetime.Scoped);
builder.Services.AddMvc();
// Register custom application dependencies.
builder.Services.AddScoped<IMyCustomRepository, MyCustomSQLRepository>();
O padrão mais comum ao registrar tipos em um contêiner IoC é registrar um par de tipos — uma interface e sua classe de implementação relacionada. Em seguida, quando você solicita um objeto do contêiner IoC através de qualquer construtor, você solicita um objeto de um determinado tipo de interface. Por exemplo, no exemplo anterior, a última linha afirma que quando qualquer um dos seus construtores tem uma dependência de IMyCustomRepository (interface ou abstração), o contêiner IoC injetará uma instância da classe de implementação MyCustomSQLServerRepository.
Usar a biblioteca Scrutor para registro automático de tipos
Ao usar DI no .NET, talvez você queira ser capaz de verificar um assembly e registrar automaticamente seus tipos por convenção. Esse recurso não está disponível no ASP.NET Core. No entanto, você pode usar a biblioteca Scrutor para isso. Essa abordagem é conveniente quando você tem dezenas de tipos que precisam ser registrados em seu contêiner IoC.
Recursos adicionais
Mateus Rei. Registrando serviços com o Scrutor
https://www.mking.net/blog/registering-services-with-scrutorKristian Hellang. Escrutinador. Repositório GitHub.
https://github.com/khellang/Scrutor
Usar o Autofac como um contêiner de IoC
Você também pode usar contêineres IoC adicionais e conectá-los ao pipeline ASP.NET Core, como no microsserviço de pedidos no eShopOnContainers, que usa Autofac. Ao usar o Autofac, você normalmente registra os tipos por meio de módulos, que permitem dividir os tipos de registro entre vários arquivos, dependendo de onde seus tipos estão, assim como você pode ter os tipos de aplicativos distribuídos em várias bibliotecas de classes.
Por exemplo, a seguir está o módulo de aplicativo Autofac para o projeto de API Web Ordering.API com os tipos que você deseja injetar.
public class ApplicationModule : Autofac.Module
{
public string QueriesConnectionString { get; }
public ApplicationModule(string qconstr)
{
QueriesConnectionString = qconstr;
}
protected override void Load(ContainerBuilder builder)
{
builder.Register(c => new OrderQueries(QueriesConnectionString))
.As<IOrderQueries>()
.InstancePerLifetimeScope();
builder.RegisterType<BuyerRepository>()
.As<IBuyerRepository>()
.InstancePerLifetimeScope();
builder.RegisterType<OrderRepository>()
.As<IOrderRepository>()
.InstancePerLifetimeScope();
builder.RegisterType<RequestManager>()
.As<IRequestManager>()
.InstancePerLifetimeScope();
}
}
Autofac também tem um recurso para digitalizar montagens e registrar tipos por convenções de nome.
O processo e os conceitos de registro são muito semelhantes à maneira como você pode registrar tipos com o contêiner IoC ASP.NET Core integrado, mas a sintaxe ao usar o Autofac é um pouco diferente.
No código de exemplo, a abstração IOrderRepository é registrada junto com a classe de implementação OrderRepository. Isso significa que sempre que um construtor estiver declarando uma dependência por meio da abstração ou interface IOrderRepository, o contêiner IoC injetará uma instância da classe OrderRepository.
O tipo de escopo da instância determina como uma instância é compartilhada entre solicitações para o mesmo serviço ou dependência. Quando uma solicitação é feita para uma dependência, o contêiner IoC pode retornar o seguinte:
Uma única instância por escopo de tempo de vida (referida no contêiner IoC ASP.NET Core como escopo).
Uma nova instância por dependência (referida no contêiner ASP.NET Core IoC como transitória).
Uma única instância compartilhada entre todos os objetos usando o contêiner IoC (referido no contêiner IoC ASP.NET Core como singleton).
Recursos adicionais
Introdução à Injeção de Dependência no ASP.NET Core
https://learn.microsoft.com/aspnet/core/fundamentals/dependency-injectionAutofac. Documentação oficial.
https://docs.autofac.org/en/latest/Comparando ASP.NET vida útil do serviço de contêiner Core IoC com escopos de instância de contêiner Autofac IoC - Cesar de la Torre.
https://devblogs.microsoft.com/cesardelatorre/comparing-asp-net-core-ioc-service-life-times-and-autofac-ioc-instance-scopes/
Implementar os padrões Command e Command Handler
No exemplo DI-through-constructor mostrado na seção anterior, o contêiner IoC estava injetando repositórios por meio de um construtor em uma classe. Mas exatamente onde eles foram injetados? Em uma API da Web simples (por exemplo, o microsserviço de catálogo no eShopOnContainers), você os injeta no nível dos controladores MVC, em um construtor de controlador, como parte do pipeline de solicitação do ASP.NET Core. No entanto, no código inicial desta seção (a classe CreateOrderCommandHandler do serviço Ordering.API em eShopOnContainers), a injeção de dependências é feita por meio do construtor de um manipulador de comando específico. Vamos explicar o que é um manipulador de comandos e por que você gostaria de usá-lo.
O padrão Command está intrinsecamente relacionado ao padrão CQRS introduzido anteriormente neste guia. O CQRS tem dois lados. A primeira área são as consultas, usando consultas simplificadas com o Dapper micro ORM, que foi explicado anteriormente. A segunda área são os comandos, que são o ponto de partida para as transações, e o canal de entrada de fora do serviço.
Como mostrado na Figura 7-24, o padrão é baseado na aceitação de comandos do lado do cliente, processando-os com base nas regras do modelo de domínio e, finalmente, persistindo os estados com transações.
Figura 7-24. Visão de alto nível dos comandos ou "lado transacional" em um padrão CQRS
A Figura 7-24 mostra que o aplicativo de interface do usuário envia um comando por meio da API que chega a um CommandHandler
, que depende do modelo de domínio e da infraestrutura, para atualizar o banco de dados.
A classe de comando
Um comando é uma solicitação para que o sistema execute uma ação que altera o estado do sistema. Os comandos são imperativos e devem ser processados apenas uma vez.
Como os comandos são imperativos, eles geralmente são nomeados com um verbo no humor imperativo (por exemplo, "criar" ou "atualizar"), e podem incluir o tipo agregado, como CreateOrderCommand. Ao contrário de um evento, um comando não é um fato do passado; trata-se apenas de um pedido, pelo que pode ser recusado.
Os comandos podem ser originados da interface do usuário como resultado de um usuário iniciar uma solicitação ou de um gerenciador de processos quando o gerenciador de processos está direcionando uma agregação para executar uma ação.
Uma característica importante de um comando é que ele deve ser processado apenas uma vez por um único recetor. Isso ocorre porque um comando é uma única ação ou transação que você deseja executar no aplicativo. Por exemplo, o mesmo comando de criação de ordem não deve ser processado mais de uma vez. Esta é uma diferença importante entre comandos e eventos. Os eventos podem ser processados várias vezes, porque muitos sistemas ou microsserviços podem estar interessados no evento.
Além disso, é importante que um comando seja processado apenas uma vez, caso o comando não seja idempotente. Um comando é idempotente se puder ser executado várias vezes sem alterar o resultado, seja por causa da natureza do comando, seja por causa da maneira como o sistema lida com o comando.
É uma boa prática tornar seus comandos e atualizações idempotentes quando faz sentido sob as regras de negócios e invariantes do seu domínio. Por exemplo, para usar o mesmo exemplo, se por qualquer motivo (lógica de repetição, hacking, etc.) o mesmo comando CreateOrder chegar ao seu sistema várias vezes, você deve ser capaz de identificá-lo e garantir que você não crie vários pedidos. Para fazer isso, você precisa anexar algum tipo de identidade nas operações e identificar se o comando ou atualização já foi processado.
Você envia um comando para um único recetor; você não publica um comando. A publicação é para eventos que afirmam um fato — que algo aconteceu e pode ser interessante para os recetores de eventos. No caso de eventos, a editora não tem preocupações sobre quais recetores recebem o evento ou o que eles fazem. Mas os eventos de domínio ou integração são uma história diferente já introduzida nas seções anteriores.
Um comando é implementado com uma classe que contém campos de dados ou coleções com todas as informações necessárias para executar esse comando. Um comando é um tipo especial de Data Transfer Object (DTO), que é usado especificamente para solicitar alterações ou transações. O comando em si é baseado exatamente nas informações necessárias para processar o comando, e nada mais.
O exemplo a seguir mostra a classe simplificada CreateOrderCommand
. Este é um comando imutável que é usado no microsserviço de pedidos no eShopOnContainers.
// DDD and CQRS patterns comment: Note that it is recommended to implement immutable Commands
// In this case, its immutability is achieved by having all the setters as private
// plus only being able to update the data just once, when creating the object through its constructor.
// References on Immutable Commands:
// http://cqrs.nu/Faq
// https://docs.spine3.org/motivation/immutability.html
// http://blog.gauffin.org/2012/06/griffin-container-introducing-command-support/
// https://learn.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/how-to-implement-a-lightweight-class-with-auto-implemented-properties
[DataContract]
public class CreateOrderCommand
: IRequest<bool>
{
[DataMember]
private readonly List<OrderItemDTO> _orderItems;
[DataMember]
public string UserId { get; private set; }
[DataMember]
public string UserName { get; private set; }
[DataMember]
public string City { get; private set; }
[DataMember]
public string Street { get; private set; }
[DataMember]
public string State { get; private set; }
[DataMember]
public string Country { get; private set; }
[DataMember]
public string ZipCode { get; private set; }
[DataMember]
public string CardNumber { get; private set; }
[DataMember]
public string CardHolderName { get; private set; }
[DataMember]
public DateTime CardExpiration { get; private set; }
[DataMember]
public string CardSecurityNumber { get; private set; }
[DataMember]
public int CardTypeId { get; private set; }
[DataMember]
public IEnumerable<OrderItemDTO> OrderItems => _orderItems;
public CreateOrderCommand()
{
_orderItems = new List<OrderItemDTO>();
}
public CreateOrderCommand(List<BasketItem> basketItems, string userId, string userName, string city, string street, string state, string country, string zipcode,
string cardNumber, string cardHolderName, DateTime cardExpiration,
string cardSecurityNumber, int cardTypeId) : this()
{
_orderItems = basketItems.ToOrderItemsDTO().ToList();
UserId = userId;
UserName = userName;
City = city;
Street = street;
State = state;
Country = country;
ZipCode = zipcode;
CardNumber = cardNumber;
CardHolderName = cardHolderName;
CardExpiration = cardExpiration;
CardSecurityNumber = cardSecurityNumber;
CardTypeId = cardTypeId;
CardExpiration = cardExpiration;
}
public class OrderItemDTO
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public decimal UnitPrice { get; set; }
public decimal Discount { get; set; }
public int Units { get; set; }
public string PictureUrl { get; set; }
}
}
Basicamente, a classe de comando contém todos os dados necessários para executar uma transação comercial usando os objetos de modelo de domínio. Assim, os comandos são simplesmente estruturas de dados que contêm dados somente leitura e nenhum comportamento. O nome do comando indica sua finalidade. Em muitas linguagens como C#, os comandos são representados como classes, mas não são classes verdadeiras no sentido real orientado a objetos.
Como característica adicional, os comandos são imutáveis, porque o uso esperado é que eles sejam processados diretamente pelo modelo de domínio. Eles não precisam mudar durante sua vida útil projetada. Em uma classe C#, a imutabilidade pode ser alcançada por não ter nenhum setters ou outros métodos que alterem o estado interno.
Lembre-se de que, se você pretende ou espera que os comandos passem por um processo de serialização/desserialização, as propriedades devem ter um setter privado e o [DataMember]
atributo (ou [JsonProperty]
). Caso contrário, o desserializador não poderá reconstruir o objeto no destino com os valores necessários. Você também pode usar propriedades verdadeiramente somente leitura se a classe tiver um construtor com parâmetros para todas as propriedades, com a convenção de nomenclatura camelCase usual, e anotar o construtor como [JsonConstructor]
. No entanto, esta opção requer mais código.
Por exemplo, a classe de comando para criar uma ordem é provavelmente semelhante em termos de dados à ordem que você deseja criar, mas você provavelmente não precisa dos mesmos atributos. Por exemplo, CreateOrderCommand
não tem um ID de pedido, porque o pedido ainda não foi criado.
Muitas classes de comando podem ser simples, exigindo apenas alguns campos sobre algum estado que precisa ser alterado. Esse seria o caso se você estiver apenas alterando o status de um pedido de "em processo" para "pago" ou "enviado" usando um comando semelhante ao seguinte:
[DataContract]
public class UpdateOrderStatusCommand
:IRequest<bool>
{
[DataMember]
public string Status { get; private set; }
[DataMember]
public string OrderId { get; private set; }
[DataMember]
public string BuyerIdentityGuid { get; private set; }
}
Alguns desenvolvedores separam seus objetos de solicitação de interface do usuário de seus DTOs de comando, mas isso é apenas uma questão de preferência. É uma separação tediosa com pouco valor adicional, e os objetos são quase exatamente a mesma forma. Por exemplo, no eShopOnContainers, alguns comandos vêm diretamente do lado do cliente.
A classe Command handler
Você deve implementar uma classe específica do manipulador de comandos para cada comando. É assim que o padrão funciona, e é onde você usará o objeto de comando, os objetos de domínio e os objetos de repositório de infraestrutura. O manipulador de comandos é, de fato, o coração da camada de aplicativo em termos de CQRS e DDD. No entanto, toda a lógica de domínio deve estar contida nas classes de domínio — dentro das raízes agregadas (entidades raiz), entidades filhas ou serviços de domínio, mas não dentro do manipulador de comandos, que é uma classe da camada de aplicativo.
A classe do manipulador de comandos oferece um forte trampolim no caminho para alcançar o Princípio de Responsabilidade Única (SRP) mencionado em uma seção anterior.
Um manipulador de comandos recebe um comando e obtém um resultado da agregação usada. O resultado deve ser a execução bem-sucedida do comando ou uma exceção. No caso de uma exceção, o estado do sistema deve permanecer inalterado.
O manipulador de comandos geralmente executa as seguintes etapas:
Ele recebe o objeto de comando, como um DTO (do mediador ou outro objeto de infraestrutura).
Ele valida que o comando é válido (se não validado pelo mediador).
Ele instancia a instância raiz agregada que é o destino do comando atual.
Ele executa o método na instância raiz agregada, obtendo os dados necessários do comando.
Ele persiste o novo estado da agregação em seu banco de dados relacionado. Esta última operação é a transação real.
Normalmente, um manipulador de comandos lida com uma única agregação impulsionada por sua raiz agregada (entidade raiz). Se várias agregações devem ser afetadas pela receção de um único comando, você pode usar eventos de domínio para propagar estados ou ações em várias agregações.
O ponto importante aqui é que, quando um comando está sendo processado, toda a lógica de domínio deve estar dentro do modelo de domínio (os agregados), totalmente encapsulada e pronta para testes de unidade. O manipulador de comandos atua apenas como uma maneira de obter o modelo de domínio do banco de dados e, como etapa final, para dizer à camada de infraestrutura (repositórios) para persistir as alterações quando o modelo for alterado. A vantagem dessa abordagem é que você pode refatorar a lógica de domínio em um modelo de domínio isolado, totalmente encapsulado, rico e comportamental sem alterar o código nas camadas de aplicativo ou infraestrutura, que são o nível de encanamento (manipuladores de comando, API Web, repositórios, etc.).
Quando os manipuladores de comandos ficam complexos, com muita lógica, isso pode ser um cheiro de código. Revise-os e, se encontrar lógica de domínio, refatore o código para mover esse comportamento de domínio para os métodos dos objetos de domínio (a raiz agregada e a entidade filha).
Como exemplo de uma classe de manipulador de comando, o código a seguir mostra a mesma CreateOrderCommandHandler
classe que você viu no início deste capítulo. Nesse caso, ele também destaca o método Handle e as operações com os objetos/agregações do modelo de domínio.
public class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, bool>
{
private readonly IOrderRepository _orderRepository;
private readonly IIdentityService _identityService;
private readonly IMediator _mediator;
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
private readonly ILogger<CreateOrderCommandHandler> _logger;
// Using DI to inject infrastructure persistence Repositories
public CreateOrderCommandHandler(IMediator mediator,
IOrderingIntegrationEventService orderingIntegrationEventService,
IOrderRepository orderRepository,
IIdentityService identityService,
ILogger<CreateOrderCommandHandler> logger)
{
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
_identityService = identityService ?? throw new ArgumentNullException(nameof(identityService));
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<bool> Handle(CreateOrderCommand message, CancellationToken cancellationToken)
{
// Add Integration event to clean the basket
var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(message.UserId);
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStartedIntegrationEvent);
// Add/Update the Buyer AggregateRoot
// DDD patterns comment: Add child entities and value-objects through the Order Aggregate-Root
// methods and constructor so validations, invariants and business logic
// make sure that consistency is preserved across the whole aggregate
var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode);
var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration);
foreach (var item in message.OrderItems)
{
order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Units);
}
_logger.LogInformation("----- Creating Order - Order: {@Order}", order);
_orderRepository.Add(order);
return await _orderRepository.UnitOfWork
.SaveEntitiesAsync(cancellationToken);
}
}
Estas são etapas adicionais que um manipulador de comandos deve seguir:
Use os dados do comando para operar com os métodos e o comportamento da raiz agregada.
Internamente dentro dos objetos de domínio, gere eventos de domínio enquanto a transação é executada, mas isso é transparente do ponto de vista do manipulador de comandos.
Se o resultado da operação agregada for bem-sucedido e após a conclusão da transação, gere eventos de integração. (Eles também podem ser gerados por classes de infraestrutura, como repositórios.)
Recursos adicionais
Marcos Seemann. Nos limites, os aplicativos não são orientados a objetos
https://blog.ploeh.dk/2011/05/31/AttheBoundaries,ApplicationsareNotObject-Oriented/Comandos e eventos
https://cqrs.nu/faq/Command%20and%20EventsO que faz um manipulador de comandos?
https://cqrs.nu/faq/Command%20HandlersJimmy Bogard. Padrões de Comando de Domínio – Manipuladores
https://jimmybogard.com/domain-command-patterns-handlers/Jimmy Bogard. Padrões de Comando de Domínio – Validação
https://jimmybogard.com/domain-command-patterns-validation/
O pipeline do processo de comando: como acionar um manipulador de comandos
A próxima pergunta é como invocar um manipulador de comando. Você pode chamá-lo manualmente de cada controlador ASP.NET Core relacionado. No entanto, essa abordagem seria demasiado associada e não é a ideal.
As outras duas opções principais, que são as opções recomendadas, são:
Através de um artefato padrão Mediador na memória.
Com uma fila de mensagens assíncrona, entre controladores e manipuladores.
Usar o padrão Mediador (na memória) no pipeline de comandos
Como mostrado na Figura 7-25, em uma abordagem CQRS, você usa um mediador inteligente, semelhante a um barramento na memória, que é inteligente o suficiente para redirecionar para o manipulador de comandos correto com base no tipo de comando ou DTO que está sendo recebido. As setas pretas simples entre componentes representam as dependências entre objetos (em muitos casos, injetadas através de DI) com suas interações relacionadas.
Figura 7-25. Usando o padrão Mediator em processo em um único microsserviço CQRS
O diagrama acima mostra um zoom da imagem 7-24: o controlador ASP.NET Core envia o comando para o pipeline de comandos do MediatR, para que eles cheguem ao manipulador apropriado.
A razão pela qual o uso do padrão Mediator faz sentido é que, em aplicativos corporativos, o processamento de solicitações pode ficar complicado. Você deseja ser capaz de adicionar um número aberto de preocupações transversais, como registro, validações, auditoria e segurança. Nesses casos, você pode confiar em um pipeline de mediador (consulte Padrão de mediador) para fornecer um meio para esses comportamentos extras ou preocupações transversais.
Um mediador é um objeto que encapsula o "como" desse processo: ele coordena a execução com base no estado, na maneira como um manipulador de comandos é invocado ou na carga que você fornece ao manipulador. Com um componente mediador, você pode aplicar preocupações transversais de forma centralizada e transparente aplicando decoradores (ou comportamentos de pipeline desde MediatR 3). Para obter mais informações, consulte o padrão Decorador.
Decoradores e comportamentos são semelhantes à Programação Orientada a Aspetos (AOP), aplicada apenas a um pipeline de processo específico gerenciado pelo componente mediador. Os aspetos no AOP que implementam preocupações transversais são aplicados com base em tecelões de aspeto injetados em tempo de compilação ou com base na intercetação de chamada de objeto. Ambas as abordagens típicas de AOP às vezes são ditas para funcionar "como magia", porque não é fácil ver como AOP faz seu trabalho. Ao lidar com problemas sérios ou bugs, o AOP pode ser difícil de depurar. Por outro lado, estes decoradores/comportamentos são explícitos e aplicados apenas no contexto do mediador, pelo que a depuração é muito mais previsível e fácil.
Por exemplo, no microsserviço de pedidos eShopOnContainers, tem uma implementação de dois comportamentos de exemplo, uma classe LogBehavior e uma classe ValidatorBehavior . A implementação dos comportamentos é explicada na próxima seção, mostrando como o eShopOnContainers usa comportamentos MediatR.
Usar filas de mensagens (out-of-proc) no pipeline do comando
Outra opção é usar mensagens assíncronas com base em agentes ou filas de mensagens, como mostra a Figura 7-26. Essa opção também pode ser combinada com o componente mediador imediatamente antes do manipulador de comandos.
Figura 7-26. Usando filas de mensagens (fora do processo e comunicação entre processos) com comandos CQRS
O pipeline do comando também pode ser manipulado por uma fila de mensagens de alta disponibilidade para entregar os comandos ao manipulador apropriado. Usar filas de mensagens para aceitar os comandos pode complicar ainda mais o pipeline do comando, porque você provavelmente precisará dividir o pipeline em dois processos conectados através da fila de mensagens externas. Ainda assim, ele deve ser usado se você precisar melhorar a escalabilidade e o desempenho com base em mensagens assíncronas. Considere que, no caso da Figura 7-26, o controlador apenas posta a mensagem de comando na fila e retorna. Em seguida, os manipuladores de comando processam as mensagens em seu próprio ritmo. Esse é um grande benefício das filas: a fila de mensagens pode atuar como um buffer em casos em que a hiperescalabilidade é necessária, como para estoques ou qualquer outro cenário com um alto volume de dados de entrada.
No entanto, devido à natureza assíncrona das filas de mensagens, você precisa descobrir como se comunicar com o aplicativo cliente sobre o sucesso ou falha do processo do comando. Como regra geral, você nunca deve usar os comandos "disparar e esquecer". Todo aplicativo de negócios precisa saber se um comando foi processado com êxito ou, pelo menos, validado e aceito.
Assim, ser capaz de responder ao cliente depois de validar uma mensagem de comando que foi submetida a uma fila assíncrona adiciona complexidade ao seu sistema, em comparação com um processo de comando em processo que retorna o resultado da operação depois de executar a transação. Usando filas, talvez seja necessário retornar o resultado do processo de comando por meio de outras mensagens de resultado de operação, o que exigirá componentes adicionais e comunicação personalizada em seu sistema.
Além disso, os comandos assíncronos são comandos unidirecionais, que em muitos casos podem não ser necessários, como é explicado na seguinte troca interessante entre Burtsev Alexey e Greg Young em uma conversa online:
[Burtsev Alexey] Eu encontro muitos códigos onde as pessoas usam manipulação de comando assíncrono ou mensagens de comando unidirecional sem qualquer razão para fazê-lo (eles não estão fazendo alguma operação longa, eles não estão executando código assíncrono externo, eles nem mesmo limite entre aplicativos para estar usando barramento de mensagem). Por que razão introduzem esta complexidade desnecessária? E, na verdade, eu não vi um exemplo de código CQRS com manipuladores de comando de bloqueio até agora, embora funcione muito bem na maioria dos casos.
[Greg Jovem] [...] um comando assíncrono não existe; na verdade, é outro evento. Se eu tenho que aceitar o que você me envia e levantar um evento se eu discordo, não é mais você me dizendo para fazer algo [ou seja, não é um comando]. É você me dizendo que algo foi feito. Esta parece ser uma pequena diferença à primeira vista, mas tem muitas implicações.
Os comandos assíncronos aumentam muito a complexidade de um sistema, porque não há uma maneira simples de indicar falhas. Portanto, comandos assíncronos não são recomendados, exceto quando os requisitos de dimensionamento são necessários ou, em casos especiais, ao comunicar os microsserviços internos por meio de mensagens. Nesses casos, você deve projetar um sistema de relatório e recuperação separado para falhas.
Na versão inicial do eShopOnContainers, decidiu-se usar o processamento de comandos síncronos, iniciado a partir de solicitações HTTP e orientado pelo padrão Mediator. Isso facilmente permite que você retorne o sucesso ou falha do processo, como na implementação CreateOrderCommandHandler .
Em qualquer caso, essa deve ser uma decisão baseada nos requisitos de negócios do seu aplicativo ou microsserviço.
Implementar o pipeline do processo de comando com um padrão mediador (MediatR)
Como um exemplo de implementação, este guia propõe o uso do pipeline em processo com base no padrão Mediator para direcionar a ingestão de comandos e rotear comandos, na memória, para os manipuladores de comando corretos. O guia também propõe a aplicação de comportamentos para separar preocupações transversais.
Para implementação no .NET, há várias bibliotecas de código aberto disponíveis que implementam o padrão Mediator. A biblioteca usada neste guia é a biblioteca de código aberto MediatR (criada por Jimmy Bogard), mas você pode usar outra abordagem. MediatR é uma biblioteca pequena e simples que permite processar mensagens na memória como um comando, enquanto aplica decoradores ou comportamentos.
O uso do padrão Mediator ajuda a reduzir o acoplamento e a isolar as preocupações do trabalho solicitado, enquanto se conecta automaticamente ao manipulador que executa esse trabalho — neste caso, aos manipuladores de comando.
Outra boa razão para usar o padrão Mediador foi explicada por Jimmy Bogard ao rever este guia:
Eu acho que pode valer a pena mencionar os testes aqui - ele fornece uma boa janela consistente para o comportamento do seu sistema. Solicitação-entrada, resposta-saída. Achamos esse aspeto bastante valioso na construção de testes de comportamento consistente.
Primeiro, vamos examinar um exemplo de controlador WebAPI onde você realmente usaria o objeto mediador. Se você não estivesse usando o objeto mediador, precisaria injetar todas as dependências para esse controlador, coisas como um objeto logger e outros. Portanto, o construtor seria complicado. Por outro lado, se você usar o objeto mediator, o construtor do seu controlador pode ser muito mais simples, com apenas algumas dependências em vez de muitas dependências se você tivesse uma por operação transversal, como no exemplo a seguir:
public class MyMicroserviceController : Controller
{
public MyMicroserviceController(IMediator mediator,
IMyMicroserviceQueries microserviceQueries)
{
// ...
}
}
Você pode ver que o mediador fornece um construtor de controlador de API Web limpo e enxuto. Além disso, dentro dos métodos do controlador, o código para enviar um comando para o objeto mediador é quase uma linha:
[Route("new")]
[HttpPost]
public async Task<IActionResult> ExecuteBusinessOperation([FromBody]RunOpCommand
runOperationCommand)
{
var commandResult = await _mediator.SendAsync(runOperationCommand);
return commandResult ? (IActionResult)Ok() : (IActionResult)BadRequest();
}
Implementar comandos idempotentes
No eShopOnContainers, um exemplo mais avançado do que o acima é o envio de um objeto CreateOrderCommand do microsserviço Ordering . Mas como o processo de negócios de Ordenação é um pouco mais complexo e, no nosso caso, ele realmente começa no microsserviço Basket, essa ação de enviar o objeto CreateOrderCommand é executada a partir de um manipulador de eventos de integração chamado UserCheckoutAcceptedIntegrationEventHandler em vez de um controlador WebAPI simples chamado do aplicativo cliente, como no exemplo mais simples anterior.
No entanto, a ação de enviar o comando para MediatR é bastante semelhante, como mostrado no código a seguir.
var createOrderCommand = new CreateOrderCommand(eventMsg.Basket.Items,
eventMsg.UserId, eventMsg.City,
eventMsg.Street, eventMsg.State,
eventMsg.Country, eventMsg.ZipCode,
eventMsg.CardNumber,
eventMsg.CardHolderName,
eventMsg.CardExpiration,
eventMsg.CardSecurityNumber,
eventMsg.CardTypeId);
var requestCreateOrder = new IdentifiedCommand<CreateOrderCommand,bool>(createOrderCommand,
eventMsg.RequestId);
result = await _mediator.Send(requestCreateOrder);
No entanto, este caso também é um pouco mais avançado porque também estamos implementando comandos idempotentes. O processo CreateOrderCommand deve ser idempotente, portanto, se a mesma mensagem vier duplicada através da rede, por qualquer motivo, como tentativas, a mesma ordem comercial será processada apenas uma vez.
Isso é implementado encapsulando o comando de negócios (neste caso, CreateOrderCommand) e incorporando-o em um IdentifiedCommand genérico, que é rastreado por um ID de cada mensagem que vem através da rede que precisa ser idempotente.
No código abaixo, você pode ver que o IdentifiedCommand nada mais é do que um DTO com e ID mais o objeto de comando de negócios encapsulado.
public class IdentifiedCommand<T, R> : IRequest<R>
where T : IRequest<R>
{
public T Command { get; }
public Guid Id { get; }
public IdentifiedCommand(T command, Guid id)
{
Command = command;
Id = id;
}
}
Em seguida, o CommandHandler para o IdentifiedCommand chamado IdentifiedCommandHandler.cs basicamente verificará se o ID que vem como parte da mensagem já existe em uma tabela. Se já existir, esse comando não será processado novamente, então ele se comporta como um comando idempotente. Esse código de infraestrutura é executado pela chamada de _requestManager.ExistAsync
método abaixo.
// IdentifiedCommandHandler.cs
public class IdentifiedCommandHandler<T, R> : IRequestHandler<IdentifiedCommand<T, R>, R>
where T : IRequest<R>
{
private readonly IMediator _mediator;
private readonly IRequestManager _requestManager;
private readonly ILogger<IdentifiedCommandHandler<T, R>> _logger;
public IdentifiedCommandHandler(
IMediator mediator,
IRequestManager requestManager,
ILogger<IdentifiedCommandHandler<T, R>> logger)
{
_mediator = mediator;
_requestManager = requestManager;
_logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
}
/// <summary>
/// Creates the result value to return if a previous request was found
/// </summary>
/// <returns></returns>
protected virtual R CreateResultForDuplicateRequest()
{
return default(R);
}
/// <summary>
/// This method handles the command. It just ensures that no other request exists with the same ID, and if this is the case
/// just enqueues the original inner command.
/// </summary>
/// <param name="message">IdentifiedCommand which contains both original command & request ID</param>
/// <returns>Return value of inner command or default value if request same ID was found</returns>
public async Task<R> Handle(IdentifiedCommand<T, R> message, CancellationToken cancellationToken)
{
var alreadyExists = await _requestManager.ExistAsync(message.Id);
if (alreadyExists)
{
return CreateResultForDuplicateRequest();
}
else
{
await _requestManager.CreateRequestForCommandAsync<T>(message.Id);
try
{
var command = message.Command;
var commandName = command.GetGenericTypeName();
var idProperty = string.Empty;
var commandId = string.Empty;
switch (command)
{
case CreateOrderCommand createOrderCommand:
idProperty = nameof(createOrderCommand.UserId);
commandId = createOrderCommand.UserId;
break;
case CancelOrderCommand cancelOrderCommand:
idProperty = nameof(cancelOrderCommand.OrderNumber);
commandId = $"{cancelOrderCommand.OrderNumber}";
break;
case ShipOrderCommand shipOrderCommand:
idProperty = nameof(shipOrderCommand.OrderNumber);
commandId = $"{shipOrderCommand.OrderNumber}";
break;
default:
idProperty = "Id?";
commandId = "n/a";
break;
}
_logger.LogInformation(
"----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})",
commandName,
idProperty,
commandId,
command);
// Send the embedded business command to mediator so it runs its related CommandHandler
var result = await _mediator.Send(command, cancellationToken);
_logger.LogInformation(
"----- Command result: {@Result} - {CommandName} - {IdProperty}: {CommandId} ({@Command})",
result,
commandName,
idProperty,
commandId,
command);
return result;
}
catch
{
return default(R);
}
}
}
}
Como o IdentifiedCommand age como o envelope de um comando de negócios, quando o comando de negócios precisa ser processado porque não é um ID repetido, ele pega esse comando de negócios interno e o reenvia ao Mediator, como na última parte do código mostrado acima ao executar _mediator.Send(message.Command)
, a partir do IdentifiedCommandHandler.cs.
Ao fazer isso, ele vinculará e executará o manipulador de comandos de negócios, neste caso, o CreateOrderCommandHandler, que está executando transações no banco de dados Ordenando, conforme mostrado no código a seguir.
// CreateOrderCommandHandler.cs
public class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, bool>
{
private readonly IOrderRepository _orderRepository;
private readonly IIdentityService _identityService;
private readonly IMediator _mediator;
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
private readonly ILogger<CreateOrderCommandHandler> _logger;
// Using DI to inject infrastructure persistence Repositories
public CreateOrderCommandHandler(IMediator mediator,
IOrderingIntegrationEventService orderingIntegrationEventService,
IOrderRepository orderRepository,
IIdentityService identityService,
ILogger<CreateOrderCommandHandler> logger)
{
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
_identityService = identityService ?? throw new ArgumentNullException(nameof(identityService));
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<bool> Handle(CreateOrderCommand message, CancellationToken cancellationToken)
{
// Add Integration event to clean the basket
var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(message.UserId);
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStartedIntegrationEvent);
// Add/Update the Buyer AggregateRoot
// DDD patterns comment: Add child entities and value-objects through the Order Aggregate-Root
// methods and constructor so validations, invariants and business logic
// make sure that consistency is preserved across the whole aggregate
var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode);
var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration);
foreach (var item in message.OrderItems)
{
order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Units);
}
_logger.LogInformation("----- Creating Order - Order: {@Order}", order);
_orderRepository.Add(order);
return await _orderRepository.UnitOfWork
.SaveEntitiesAsync(cancellationToken);
}
}
Registrar os tipos usados pelo MediatR
Para que o MediatR esteja ciente de suas classes de manipulador de comando, você precisa registrar as classes mediator e as classes de manipulador de comando em seu contêiner IoC. Por padrão, o MediatR usa o Autofac como o contêiner IoC, mas você também pode usar o contêiner IoC ASP.NET Core interno ou qualquer outro contêiner suportado pelo MediatR.
O código a seguir mostra como registrar os tipos e comandos do Mediator ao usar módulos Autofac.
public class MediatorModule : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly)
.AsImplementedInterfaces();
// Register all the Command classes (they implement IRequestHandler)
// in assembly holding the Commands
builder.RegisterAssemblyTypes(typeof(CreateOrderCommand).GetTypeInfo().Assembly)
.AsClosedTypesOf(typeof(IRequestHandler<,>));
// Other types registration
//...
}
}
É aqui que "a magia acontece" com o MediatR.
Como cada manipulador de comando implementa a interface genérica IRequestHandler<T>
, quando você registra os assemblies usando RegisteredAssemblyTypes
o método, todos os tipos marcados como IRequestHandler
também são registrados com seu Commands
. Por exemplo:
public class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, bool>
{
Esse é o código que correlaciona comandos com manipuladores de comando. O manipulador é apenas uma classe simples, mas herda de , onde T é o tipo de RequestHandler<T>
comando, e MediatR garante que ele seja invocado com a carga correta (o comando).
Aplique preocupações transversais ao processar comandos com os Comportamentos no MediatR
Há mais uma coisa: ser capaz de aplicar preocupações transversais ao pipeline de mediadores. Você também pode ver no final do código do módulo de registro Autofac como ele registra um tipo de comportamento, especificamente, uma classe LoggingBehavior personalizada e uma classe ValidatorBehavior. Mas você também pode adicionar outros comportamentos personalizados.
public class MediatorModule : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly)
.AsImplementedInterfaces();
// Register all the Command classes (they implement IRequestHandler)
// in assembly holding the Commands
builder.RegisterAssemblyTypes(
typeof(CreateOrderCommand).GetTypeInfo().Assembly).
AsClosedTypesOf(typeof(IRequestHandler<,>));
// Other types registration
//...
builder.RegisterGeneric(typeof(LoggingBehavior<,>)).
As(typeof(IPipelineBehavior<,>));
builder.RegisterGeneric(typeof(ValidatorBehavior<,>)).
As(typeof(IPipelineBehavior<,>));
}
}
Essa classe LoggingBehavior pode ser implementada como o código a seguir, que registra informações sobre o manipulador de comando que está sendo executado e se ele foi bem-sucedido ou não.
public class LoggingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger) =>
_logger = logger;
public async Task<TResponse> Handle(TRequest request,
RequestHandlerDelegate<TResponse> next)
{
_logger.LogInformation($"Handling {typeof(TRequest).Name}");
var response = await next();
_logger.LogInformation($"Handled {typeof(TResponse).Name}");
return response;
}
}
Apenas implementando essa classe de comportamento e registrando-a no pipeline (no MediatorModule acima), todos os comandos processados através do MediatR estarão registrando informações sobre a execução.
O microsserviço de pedido eShopOnContainers também aplica um segundo comportamento para validações básicas, a classe ValidatorBehavior que depende da biblioteca FluentValidation , conforme mostrado no código a seguir:
public class ValidatorBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
{
private readonly IValidator<TRequest>[] _validators;
public ValidatorBehavior(IValidator<TRequest>[] validators) =>
_validators = validators;
public async Task<TResponse> Handle(TRequest request,
RequestHandlerDelegate<TResponse> next)
{
var failures = _validators
.Select(v => v.Validate(request))
.SelectMany(result => result.Errors)
.Where(error => error != null)
.ToList();
if (failures.Any())
{
throw new OrderingDomainException(
$"Command Validation Errors for type {typeof(TRequest).Name}",
new ValidationException("Validation exception", failures));
}
var response = await next();
return response;
}
}
Aqui, o comportamento está gerando uma exceção se a validação falhar, mas você também pode retornar um objeto de resultado, contendo o resultado do comando se ele for bem-sucedido ou as mensagens de validação caso não tenha sido bem-sucedido. Isso provavelmente facilitaria a exibição dos resultados da validação para o usuário.
Em seguida, com base na biblioteca FluentValidation , você criaria a validação para os dados passados com CreateOrderCommand, como no código a seguir:
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderCommandValidator()
{
RuleFor(command => command.City).NotEmpty();
RuleFor(command => command.Street).NotEmpty();
RuleFor(command => command.State).NotEmpty();
RuleFor(command => command.Country).NotEmpty();
RuleFor(command => command.ZipCode).NotEmpty();
RuleFor(command => command.CardNumber).NotEmpty().Length(12, 19);
RuleFor(command => command.CardHolderName).NotEmpty();
RuleFor(command => command.CardExpiration).NotEmpty().Must(BeValidExpirationDate).WithMessage("Please specify a valid card expiration date");
RuleFor(command => command.CardSecurityNumber).NotEmpty().Length(3);
RuleFor(command => command.CardTypeId).NotEmpty();
RuleFor(command => command.OrderItems).Must(ContainOrderItems).WithMessage("No order items found");
}
private bool BeValidExpirationDate(DateTime dateTime)
{
return dateTime >= DateTime.UtcNow;
}
private bool ContainOrderItems(IEnumerable<OrderItemDTO> orderItems)
{
return orderItems.Any();
}
}
Você pode criar validações adicionais. Esta é uma maneira muito limpa e elegante de implementar suas validações de comando.
Da mesma forma, você pode implementar outros comportamentos para aspetos adicionais ou preocupações transversais que deseja aplicar aos comandos ao manipulá-los.
Recursos adicionais
O padrão mediador
- Padrão do mediador
https://en.wikipedia.org/wiki/Mediator_pattern
O padrão do decorador
- Padrão do decorador
https://en.wikipedia.org/wiki/Decorator_pattern
MediatR (Jimmy Bogard)
MediatR. Repositório GitHub.
https://github.com/jbogard/MediatRCQRS com MediatR e AutoMapper
https://lostechies.com/jimmybogard/2015/05/05/cqrs-with-mediatr-and-automapper/Coloque seus controladores em uma dieta: POSTs e comandos.
https://lostechies.com/jimmybogard/2013/12/19/put-your-controllers-on-a-diet-posts-and-commands/Dar resposta às preocupações transversais através de uma reserva de mediadores
https://lostechies.com/jimmybogard/2014/09/09/tackling-cross-cutting-concerns-with-a-mediator-pipeline/CQRS e REST: a combinação perfeita
https://lostechies.com/jimmybogard/2016/06/01/cqrs-and-rest-the-perfect-match/Exemplos de pipeline MediatR
https://lostechies.com/jimmybogard/2016/10/13/mediatr-pipeline-examples/Fixtures de teste de fatia vertical para MediatR e núcleo ASP.NET
https://lostechies.com/jimmybogard/2016/10/24/vertical-slice-test-fixtures-for-mediatr-and-asp-net-core/Extensões MediatR para Microsoft Dependency Injection lançadas
https://lostechies.com/jimmybogard/2016/07/19/mediatr-extensions-for-microsoft-dependency-injection-released/
Validação fluente
- Jeremy Skinner. FluentValidation. Repositório GitHub.
https://github.com/JeremySkinner/FluentValidation