Implementar a camada de aplicativos de microsserviço usando a API WebImplement the microservice application layer using the Web API

Usar a injeção de dependência para injetar objetos de infraestrutura em sua camada de aplicativoUse Dependency Injection to inject infrastructure objects into your application layer

Conforme mencionado anteriormente, a camada de aplicativo pode ser implementada como parte do artefato (assembly) que você está criando, tal como em um projeto de API Web ou um projeto de aplicativo Web do MVC.As mentioned previously, the application layer can be implemented as part of the artifact (assembly) you are building, such as within a Web API project or an MVC web app project. No caso de um microsserviço criado com o ASP.NET Core, a camada de aplicativo geralmente será a biblioteca da API Web.In the case of a microservice built with ASP.NET Core, the application layer will usually be your Web API library. Se quiser separar o que é proveniente do ASP.NET Core (a infraestrutura e os controladores) do código personalizado da camada de aplicativo, você também poderá colocar a camada de aplicativo em uma biblioteca de classes separada, mas isso é opcional.If you want to separate what is coming from ASP.NET Core (its infrastructure plus your controllers) from your custom application layer code, you could also place your application layer in a separate class library, but that is optional.

Por exemplo, o código da camada de aplicativo do microsserviço de ordenação é implementado diretamente como parte do projeto Ordering.API (um projeto da API Web ASP.NET Core), como mostrado na Figura 7-23.For instance, the application layer code of the ordering microservice is directly implemented as part of the Ordering.API project (an ASP.NET Core Web API project), as shown in Figure 7-23.

Captura de tela do microserviço de classificação. API no Gerenciador de Soluções.

O Gerenciador de Soluções exibição do microserviço de classificação. API, mostrando as subpastas na pasta do aplicativo: comportamentos, comandos, DomainEventHandlers, IntegrationEvents, modelos, consultas e validações.The Solution Explorer view of the Ordering.API microservice, showing the subfolders under the Application folder: Behaviors, Commands, DomainEventHandlers, IntegrationEvents, Models, Queries, and Validations.

Figura 7-23.Figure 7-23. A camada de aplicativo no projeto Ordering.API da API Web ASP.NET CoreThe application layer in the Ordering.API ASP.NET Core Web API project

O ASP.NET Core inclui um contêiner interno de IoC simples (representado pela interface IServiceProvider) que é compatível com a injeção de construtor por padrão, e o ASP.NET disponibiliza alguns serviços por meio da DI (injeção de dependência).ASP.NET Core includes a simple built-in IoC container (represented by the IServiceProvider interface) that supports constructor injection by default, and ASP.NET makes certain services available through DI. O ASP.NET Core usa o termo serviço para qualquer um dos tipos que você registra e que serão injetados pela DI.ASP.NET Core uses the term service for any of the types you register that will be injected through DI. Você configura os serviços internos do contêiner no método ConfigureServices na classe Startup do seu aplicativo.You configure the built-in container's services in the ConfigureServices method in your application's Startup class. As dependências são implementadas nos serviços que são necessários para um tipo e que você registra no contêiner de IoC.Your dependencies are implemented in the services that a type needs and that you register in the IoC container.

Normalmente, você deseja injetar dependências que implementam objetos de infraestrutura.Typically, you want to inject dependencies that implement infrastructure objects. Uma dependência típica para injetar é um repositório.A typical dependency to inject is a repository. Mas você poderá injetar qualquer outra dependência de infraestrutura que tiver.But you could inject any other infrastructure dependency that you may have. Para implementações mais simples, você injeta diretamente o objeto de padrão da Unidade de Trabalho (o objeto DbContext do EF), porque o DBContext também é a implementação dos objetos de persistência da sua infraestrutura.For simpler implementations, you could directly inject your Unit of Work pattern object (the EF DbContext object), because the DBContext is also the implementation of your infrastructure persistence objects.

Veja no exemplo a seguir como o .NET Core está injetando os objetos de repositório necessários por meio do construtor.In the following example, you can see how .NET Core is injecting the required repository objects through the constructor. A classe é um manipulador de comando, que será abordado na próxima seção.The class is a command handler, which will get covered in the next section.

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.The class uses the injected repositories to execute the transaction and persist the state changes. Não importa se a classe é um manipulador de comandos, um método de controlador da API Web ASP.NET Core ou um Serviço de aplicativo DDD.It does not matter whether that class is a command handler, an ASP.NET Core Web API controller method, or a DDD Application Service. Em última análise, ela 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.It is ultimately a simple class that uses repositories, domain entities, and other application coordination in a fashion similar to a command handler. A injeção de dependência funciona da mesma forma para todas as classes mencionadas, como no exemplo que usa a injeção de dependência com base no construtor.Dependency Injection works the same way for all the mentioned classes, as in the example using DI based on the constructor.

Registrar tipos e interfaces ou abstrações da implementação de dependênciaRegister the dependency implementation types and interfaces or abstractions

Antes de usar os objetos injetados por meio de construtores, você precisa saber em que lugar registrar as interfaces e classes que produzem os objetos injetados nas classes do aplicativo por meio da DI.Before you use the objects injected through constructors, you need to know where to register the interfaces and classes that produce the objects injected into your application classes through DI. (Como na DI baseada no construtor, conforme mostrado anteriormente).(Like DI based on the constructor, as shown previously.)

Usar o contêiner interno de IoC fornecido pelo ASP.NET CoreUse the built-in IoC container provided by ASP.NET Core

Ao usar o contêiner interno de IoC fornecido pelo ASP.NET Core, você registra os tipos que deseja injetar no método ConfigureServices no arquivo Startup.cs, como no código a seguir:When you use the built-in IoC container provided by ASP.NET Core, you register the types you want to inject in the ConfigureServices method in the Startup.cs file, as in the following code:

// Registration of types into ASP.NET Core built-in container
public void ConfigureServices(IServiceCollection services)
{
    // Register out-of-the-box framework services.
    services.AddDbContext<CatalogContext>(c =>
        c.UseSqlServer(Configuration["ConnectionString"]),
        ServiceLifetime.Scoped);

    services.AddMvc();
    // Register custom application dependencies.
    services.AddScoped<IMyCustomRepository, MyCustomSQLRepository>();
}

O padrão mais comum ao registrar tipos em um contêiner de IoC é registrar um par de tipos: uma interface e a respectiva classe de implementação.The most common pattern when registering types in an IoC container is to register a pair of types—an interface and its related implementation class. Então, quando solicita um objeto do contêiner de IoC por meio de nenhum construtor, você solicita um objeto de um determinado tipo de interface.Then when you request an object from the IoC container through any constructor, you request an object of a certain type of interface. Assim, como visto no exemplo anterior, a última linha indica que, quando qualquer um dos seus construtores tiver uma dependência em IMyCustomRepository (interface ou abstração), o contêiner de IoC injetará uma instância da classe de implementação MyCustomSQLServerRepository.For instance, in the previous example, the last line states that when any of your constructors have a dependency on IMyCustomRepository (interface or abstraction), the IoC container will inject an instance of the MyCustomSQLServerRepository implementation class.

Usar a biblioteca Scrutor para registro automático de tiposUse the Scrutor library for automatic types registration

Ao usar a DI no .NET Core, talvez seja interessante verificar um assembly e registrar automaticamente seus tipos por convenção.When using DI in .NET Core, you might want to be able to scan an assembly and automatically register its types by convention. Este recurso não está disponível atualmente no ASP.NET Core.This feature is not currently available in ASP.NET Core. No entanto, você pode usar a biblioteca Scrutor para fazer isso.However, you can use the Scrutor library for that. Essa abordagem é conveniente quando você tem muitos tipos que precisam ser registrados no contêiner de IoC.This approach is convenient when you have dozens of types that need to be registered in your IoC container.

Recursos adicionaisAdditional resources

Usar o Autofac como um contêiner de IoCUse Autofac as an IoC container

Você também pode usar mais contêineres de IoC e conectá-los no pipeline do ASP.NET Core, como no microsserviço de ordenação em eShopOnContainers, que usa o Autofac.You can also use additional IoC containers and plug them into the ASP.NET Core pipeline, as in the ordering microservice in eShopOnContainers, which uses Autofac. Ao usar Autofac, você normalmente registra os tipos por meio de módulos, que permitem dividir os tipos de registro entre vários arquivos, dependendo do local em que seus tipos estão, assim como os tipos de aplicativos podem ser distribuídos entre várias bibliotecas de classes.When using Autofac you typically register the types via modules, which allow you to split the registration types between multiple files depending on where your types are, just as you could have the application types distributed across multiple class libraries.

Por exemplo, a seguir está o módulo de aplicativo do Autofac para o projeto de API Web Ordering.API com os tipos que você pode injetar.For example, the following is the Autofac application module for the Ordering.API Web API project with the types you will want to inject.

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();
   }
}

O Autofac também tem um recurso para digitalizar assemblies e registrar tipos pelas convenções de nome.Autofac also has a feature to scan assemblies and register types by name conventions.

O processo e os conceitos de registro são muito semelhantes à forma pela qual você registra tipos com o contêiner de IoC interno do ASP.NET Core, mas a sintaxe, ao usar Autofac, é um pouco diferente.The registration process and concepts are very similar to the way you can register types with the built-in ASP.NET Core IoC container, but the syntax when using Autofac is a bit different.

No código de exemplo, a abstração IOrderRepository é registrada juntamente com a classe de implementação OrderRepository.In the example code, the abstraction IOrderRepository is registered along with the implementation class OrderRepository. Isso significa que, sempre que um construtor estiver declarando uma dependência por meio da abstração ou interface IOrderRepository, o contêiner de IoC injetará uma instância da classe OrderRepository.This means that whenever a constructor is declaring a dependency through the IOrderRepository abstraction or interface, the IoC container will inject an instance of the OrderRepository class.

O tipo de escopo da instância determina como uma instância é compartilhada entre as solicitações para o mesmo serviço ou dependência.The instance scope type determines how an instance is shared between requests for the same service or dependency. Quando uma solicitação é feita a uma dependência, o contêiner de IoC pode retornar o seguinte:When a request is made for a dependency, the IoC container can return the following:

  • Uma única instância por escopo de tempo de vida (conhecida no contêiner de IoC do ASP.NET Core como com escopo).A single instance per lifetime scope (referred to in the ASP.NET Core IoC container as scoped).

  • Uma nova instância por dependência (conhecida no contêiner de IoC do ASP.NET Core como transitória).A new instance per dependency (referred to in the ASP.NET Core IoC container as transient).

  • Uma única instância compartilhada entre todos os objetos que usam o contêiner de IoC (conhecida no contêiner de IoC do ASP.NET Core como singleton).A single instance shared across all objects using the IoC container (referred to in the ASP.NET Core IoC container as singleton).

Recursos adicionaisAdditional resources

Implementar os padrões de Comando e Manipulador de ComandosImplement the Command and Command Handler patterns

No exemplo de DI por meio de construtor, mostrado na seção anterior, o contêiner de IoC injetou repositórios por meio de um construtor em uma classe.In the DI-through-constructor example shown in the previous section, the IoC container was injecting repositories through a constructor in a class. Mas em que local eles foram exatamente injetados?But exactly where were they injected? Em uma API Web simples (por exemplo, o microserviço de catálogo em eShopOnContainers), você os injeta no nível dos controladores MVC, em um construtor de controlador, como parte do pipeline de solicitação de ASP.NET Core.In a simple Web API (for example, the catalog microservice in eShopOnContainers), you inject them at the MVC controllers' level, in a controller constructor, as part of the request pipeline of ASP.NET Core. Entretanto, 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 comandos específico.However, in the initial code of this section (the CreateOrderCommandHandler class from the Ordering.API service in eShopOnContainers), the injection of dependencies is done through the constructor of a particular command handler. Vamos explicar o que é um manipulador de comandos e por que você o usaria.Let us explain what a command handler is and why you would want to use it.

O padrão Command é intrinsecamente relacionado ao padrão CQRS, apresentado anteriormente neste guia.The Command pattern is intrinsically related to the CQRS pattern that was introduced earlier in this guide. O CQRS tem dois lados.CQRS has two sides. A primeira área é a de consultas, usando consultas simplificadas com o micro ORM Dapper, explicado anteriormente.The first area is queries, using simplified queries with the Dapper micro ORM, which was explained previously. A segunda área é a de comandos, os quais são o ponto de partida para transações e o canal de entrada do serviço.The second area is commands, which are the starting point for transactions, and the input channel from outside the service.

Como mostra a Figura 7-24, o padrão é baseado em aceitar comandos do lado do cliente, processá-los com base nas regras de modelo de domínio e, finalmente, persistir os Estados com transações.As shown in Figure 7-24, the pattern is based on accepting commands from the client-side, processing them based on the domain model rules, and finally persisting the states with transactions.

Diagrama que mostra o fluxo de dados de alto nível do cliente para o Database.

Figura 7-24.Figure 7-24. Exibição de alto nível dos comandos ou "lado transacional" em um padrão CQRSHigh-level view of the commands or "transactional side" in a CQRS pattern

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.Figure 7-24 shows that the UI app sends a command through the API that gets to a CommandHandler, that depends on the Domain model and the Infrastructure, to update the database.

A classe de comandoThe command class

Um comando é uma solicitação ao sistema para executar uma ação que altera o estado do sistema.A command is a request for the system to perform an action that changes the state of the system. Os comandos são imperativos e devem ser processados apenas uma vez.Commands are imperative, and should be processed just once.

Como são imperativos, os comandos geralmente são nomeados com um verbo no modo imperativo, por exemplo, "criar" ou "atualizar", e eles podem incluir o tipo de agregação, como CreateOrderCommand.Since commands are imperatives, they are typically named with a verb in the imperative mood (for example, "create" or "update"), and they might include the aggregate type, such as CreateOrderCommand. Ao contrário de um evento, um comando não é um fato do passado; ele é apenas uma solicitação e, portanto, pode ser recusado.Unlike an event, a command is not a fact from the past; it is only a request, and thus may be refused.

Os comandos podem se originar na interface do usuário, como resultado do início de uma solicitação por um usuário ou de um gerenciador de processos, quando ele está instruindo uma agregação a executar uma ação.Commands can originate from the UI as a result of a user initiating a request, or from a process manager when the process manager is directing an aggregate to perform an action.

Uma característica importante de um comando é que ele deve ser processado apenas uma vez por um único destinatário.An important characteristic of a command is that it should be processed just once by a single receiver. Isso porque um comando é uma ação ou transação única que você deseja executar no aplicativo.This is because a command is a single action or transaction you want to perform in the application. Por exemplo, o mesmo comando de criação de pedido não deve ser processado mais de uma vez.For example, the same order creation command should not be processed more than once. Essa é uma importante diferença entre comandos e eventos.This is an important difference between commands and events. Os eventos podem ser processados várias vezes, porque muitos sistemas ou microsserviços podem estar interessados no evento.Events may be processed multiple times, because many systems or microservices might be interested in the event.

Além disso, é importante que um comando seja processado apenas uma vez, caso ele não seja idempotente.In addition, it is important that a command be processed only once in case the command is not idempotent. Um comando será idempotente se ele puder ser executado várias vezes sem alterar o resultado, devido à natureza do comando ou por causa da forma como o sistema manipula o comando.A command is idempotent if it can be executed multiple times without changing the result, either because of the nature of the command, or because of the way the system handles the command.

É uma boa prática fazer seus comandos e atualizações idempotentes quando fizer sentido nas regras de negócios e nas invariáveis de seu domínio.It is a good practice to make your commands and updates idempotent when it makes sense under your domain's business rules and invariants. Assim, para usar o mesmo exemplo, se por algum motivo (lógica de repetição, hackers, etc.) o mesmo comando CreateOrder chegar até seu sistema diversas vezes, você deverá ser capaz de identificá-lo e ter a certeza de não criar vários pedidos.For instance, to use the same example, if for any reason (retry logic, hacking, etc.) the same CreateOrder command reaches your system multiple times, you should be able to identify it and ensure that you do not create multiple orders. Para fazer isso, você precisa anexar algum tipo de identidade nas operações e identificar se o comando ou a atualização já foi processada.To do so, you need to attach some kind of identity in the operations and identify whether the command or update was already processed.

Você envia um comando a um único destinatário; você não publica um comando.You send a command to a single receiver; you do not publish a command. A publicação serve para eventos que declaram um fato, ou seja, que algo aconteceu e pode ser interessante para destinatários de eventos.Publishing is for events that state a fact—that something has happened and might be interesting for event receivers. No caso de eventos, o publicador não tem preocupações sobre quais destinatários recebem o evento ou o que eles fazem com isso.In the case of events, the publisher has no concerns about which receivers get the event or what they do it. No entanto, os eventos de domínio ou de integração são outra história, já apresentada nas seções anteriores.But domain or integration events are a different story already introduced in previous sections.

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.A command is implemented with a class that contains data fields or collections with all the information that is needed in order to execute that command. Um comando é um tipo especial de DTO (Objeto de Transferência de Dados), usado especificamente para solicitar alterações ou transações.A command is a special kind of Data Transfer Object (DTO), one that is specifically used to request changes or transactions. O próprio comando baseia-se estritamente nas informações necessárias para processar o comando e nada mais.The command itself is based on exactly the information that is needed for processing the command, and nothing more.

O exemplo a seguir mostra a CreateOrderCommand classe simplificada.The following example shows the simplified CreateOrderCommand class. Este é um comando imutável que é usado no microsserviço de ordenação em eShopOnContainers.This is an immutable command that is used in the ordering microservice in 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://docs.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 realizar uma transação comercial, usando os objetos do modelo de domínio.Basically, the command class contains all the data you need for performing a business transaction by using the domain model objects. Assim, os comandos são simplesmente estruturas de dados que contêm dados somente leitura e nenhum comportamento.Thus, commands are simply data structures that contain read-only data, and no behavior. O nome do comando indica sua finalidade.The command's name indicates its purpose. Em várias linguagens, como C#, os comandos são representados como classes, mas eles não são verdadeiramente classes, no real sentido de serem orientados a objetos.In many languages like C#, commands are represented as classes, but they are not true classes in the real object-oriented sense.

Como uma característica adicional, os comandos são imutáveis, porque o uso esperado é que eles sejam processados diretamente pelo modelo de domínio.As an additional characteristic, commands are immutable, because the expected usage is that they are processed directly by the domain model. Eles não precisam ser alterados durante o tempo de vida projetado.They do not need to change during their projected lifetime. Em uma classe C#, a imutabilidade pode ser obtida não tendo nenhum setter ou outro método que altere o estado interno.In a C# class, immutability can be achieved by not having any setters or other methods that change the internal state.

Tenha em mente que, se você pretender ou esperar que os comandos passem por um processo de serialização/desserialização, as propriedades deverão ter um setter particular e o [DataMember] atributo (ou [JsonProperty] ).Keep in mind that if you intend or expect commands to go through a serializing/deserializing process, the properties must have a private setter, and the [DataMember] (or [JsonProperty]) attribute. Caso contrário, o desserializador não poderá reconstruir o objeto no destino com os valores necessários.Otherwise, the deserializer won't be able to reconstruct the object at the destination with the required values. 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 comum e anotar o construtor como [JsonConstructor] .You can also use truly read-only properties if the class has a constructor with parameters for all properties, with the usual camelCase naming convention, and annotate the constructor as [JsonConstructor]. No entanto, essa opção requer mais código.However, this option requires more code.

Por exemplo, a classe de comando para a criação de um pedido é, provavelmente, semelhante em relação aos dados para o pedido que você deseja criar, mas é provável que você não precise dos mesmos atributos.For example, the command class for creating an order is probably similar in terms of data to the order you want to create, but you probably do not need the same attributes. Por exemplo, não CreateOrderCommand tem uma ID de pedido, porque a ordem ainda não foi criada.For instance, CreateOrderCommand does not have an order ID, because the order has not been created yet.

Muitas classes de comando podem ser simples, exigindo apenas alguns campos de algum estado que tenha que ser alterado.Many command classes can be simple, requiring only a few fields about some state that needs to be changed. 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:That would be the case if you are just changing the status of an order from "in process" to "paid" or "shipped" by using a command similar to the following:

[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 criam os objetos de solicitação da interface do usuário separados dos DTOs do comando, mas essa é apenas uma questão de preferência.Some developers make their UI request objects separate from their command DTOs, but that is just a matter of preference. É uma separação entediante, sem muito valor adicional, e os objetos são quase exatamente a mesma forma.It is a tedious separation with not much additional value, and the objects are almost exactly the same shape. Por exemplo, no eShopOnContainers, alguns comandos vêm diretamente do lado do cliente.For instance, in eShopOnContainers, some commands come directly from the client-side.

A classe do manipulador de comandosThe Command handler class

Você deve implementar uma classe manipuladora de comandos específica para cada comando.You should implement a specific command handler class for each command. É 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.That is how the pattern works, and it's where you'll use the command object, the domain objects, and the infrastructure repository objects. O manipulador de comandos é, na verdade, a essência da camada de aplicativo em termos de CQRS e DDD.The command handler is in fact the heart of the application layer in terms of CQRS and DDD. No entanto, toda a lógica de domínio deve estar contida nas classes de domínio — dentro das raízes de agregação (entidades raiz), entidades filho ou serviços de domínio, mas não dentro do manipulador de comandos, que é uma classe da camada de aplicativo.However, all the domain logic should be contained in the domain classes—within the aggregate roots (root entities), child entities, or domain services, but not within the command handler, which is a class from the application layer.

A classe de manipulador de comando oferece um forte ponto de partida no caminho para alcançar o SRP (Princípio de Responsabilidade Único) mencionado em uma seção anterior.The command handler class offers a strong stepping stone in the way to achieve the Single Responsibility Principle (SRP) mentioned in a previous section.

Um manipulador de comandos recebe um comando e obtém um resultado da agregação que é usada.A command handler receives a command and obtains a result from the aggregate that is used. O resultado deverá ser a execução bem-sucedida do comando ou uma exceção.The result should be either successful execution of the command, or an exception. No caso de uma exceção, o estado do sistema deve permanecer inalterado.In the case of an exception, the system state should be unchanged.

O manipulador de comandos geralmente realiza as seguintes etapas:The command handler usually takes the following steps:

  • Ele recebe o objeto de comando, como um DTO (do mediador ou de outro objeto da infraestrutura).It receives the command object, like a DTO (from the mediator or other infrastructure object).

  • Ele verifica se o comando é válido (se não tiver sido validado pelo mediador).It validates that the command is valid (if not validated by the mediator).

  • Ele cria a instância da raiz de agregação que é o destino do comando atual.It instantiates the aggregate root instance that is the target of the current command.

  • Ele executa o método na instância raiz da agregação, obtendo os dados necessários do comando.It executes the method on the aggregate root instance, getting the required data from the command.

  • Ele persiste o novo estado da agregação no respectivo banco de dados.It persists the new state of the aggregate to its related database. Esta última operação é, verdadeiramente, a transação.This last operation is the actual transaction.

Normalmente, um manipulador de comandos lida com uma única agregação orientada por sua raiz de agregação (entidade de raiz).Typically, a command handler deals with a single aggregate driven by its aggregate root (root entity). Se várias agregações devem ser afetadas pela recepção de um único comando, você pode usar eventos de domínio para propagar estados ou ações entre várias agregações.If multiple aggregates should be impacted by the reception of a single command, you could use domain events to propagate states or actions across multiple aggregates.

O ponto importante aqui é que, quando um comando está sendo processado, toda a lógica do domínio deve estar dentro do modelo de domínio (as agregações), totalmente encapsulada e pronta para o teste de unidade.The important point here is that when a command is being processed, all the domain logic should be inside the domain model (the aggregates), fully encapsulated and ready for unit testing. O manipulador de comandos atua apenas como uma maneira de obter o modelo de domínio do banco de dados e como a etapa final, para informar à camada de infraestrutura (aos repositórios) para persistir as alterações quando o modelo for alterado.The command handler just acts as a way to get the domain model from the database, and as the final step, to tell the infrastructure layer (repositories) to persist the changes when the model is changed. A vantagem dessa abordagem é que você pode refatorar a lógica do domínio em um modelo de domínio isolado, totalmente encapsulado, avançado e comportamental sem alterar o código do aplicativo ou as camadas de infraestrutura, que fazem parte do nível de detalhes técnicos (manipuladores de comandos, API Web, repositórios, etc.).The advantage of this approach is that you can refactor the domain logic in an isolated, fully encapsulated, rich, behavioral domain model without changing code in the application or infrastructure layers, which are the plumbing level (command handlers, Web API, repositories, etc.).

Quando manipuladores de comandos se tornam complexos, com muita lógica, começam a ficar parecidos com código.When command handlers get complex, with too much logic, that can be a code smell. Examine-os e, se você 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 de agregação e a entidade filho).Review them, and if you find domain logic, refactor the code to move that domain behavior to the methods of the domain objects (the aggregate root and child entity).

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.As an example of a command handler class, the following code shows the same CreateOrderCommandHandler class that you saw at the beginning of this chapter. Nesse caso, ele também realça o método Handle e as operações com os objetos/agregados de modelo de domínio.In this case, it also highlights the Handle method and the operations with the domain model objects/aggregates.

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);
    }
}

Aqui estão etapas adicionais que um manipulador de comandos deve realizar:These are additional steps a command handler should take:

  • Use os dados do comando para operar com os métodos e o comportamento da raiz agregada.Use the command's data to operate with the aggregate root's methods and behavior.

  • Internamente, dentro dos objetos de domínio, acionar eventos de domínio enquanto a transação é executada, mas isso é transparente do ponto de vista de um manipulador comandos.Internally within the domain objects, raise domain events while the transaction is executed, but that is transparent from a command handler point of view.

  • Se o resultado da operação da agregação for bem-sucedido e depois que a transação for concluída, aumente os eventos de integração.If the aggregate's operation result is successful and after the transaction is finished, raise integration events. (Eles também podem ser acionados por classes de infraestrutura como repositórios).(These might also be raised by infrastructure classes like repositories.)

Recursos adicionaisAdditional resources

O pipeline de processo Comando: como disparar um manipulador de comandosThe Command process pipeline: how to trigger a command handler

A próxima pergunta é como invocar um manipulador de comandos.The next question is how to invoke a command handler. Você poderia chamá-lo manualmente em cada controlador do ASP.NET Core relacionado.You could manually call it from each related ASP.NET Core controller. No entanto, essa abordagem seria muito acoplada e não seria o ideal.However, that approach would be too coupled and is not ideal.

As outras duas opções principais, que são as opções recomendadas, são:The other two main options, which are the recommended options, are:

  • Por meio de um artefato de padrão Mediador na memória.Through an in-memory Mediator pattern artifact.

  • Com uma fila de mensagens assíncronas, entre os controladores e manipuladores.With an asynchronous message queue, in between controllers and handlers.

Usar o padrão Mediador (na memória) no pipeline de comandoUse the Mediator pattern (in-memory) in the command pipeline

Conforme mostrado na Figura 7-25, em uma abordagem CQRS você usa um mediador, 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 shown in Figure 7-25, in a CQRS approach you use an intelligent mediator, similar to an in-memory bus, which is smart enough to redirect to the right command handler based on the type of the command or DTO being received. As setas pretas simples entre componentes representam as dependências entre objetos (em muitos casos, injetadas por meio da DI) com as respectivas interações.The single black arrows between components represent the dependencies between objects (in many cases, injected through DI) with their related interactions.

Diagrama que mostra um fluxo de dados mais detalhado do cliente para o Database.

Figura 7-25.Figure 7-25. Usando o padrão Mediador no processo em um único microsserviço CQRSUsing the Mediator pattern in process in a single CQRS microservice

O diagrama acima mostra um zoom da imagem 7-24: o controlador de ASP.NET Core envia o comando para o pipeline de comando do mediador, para que eles obtenham o manipulador apropriado.The above diagram shows a zoom-in from image 7-24: the ASP.NET Core controller sends the command to MediatR's command pipeline, so they get to the appropriate handler.

O motivo pelo qual o uso do padrão Mediador faz sentido é porque, em aplicativos empresariais, as solicitações de processamento podem ficar complicadas.The reason that using the Mediator pattern makes sense is that in enterprise applications, the processing requests can get complicated. Você almeja adicionar um número indefinido de interesses transversais como registro em log, validações, auditoria e segurança.You want to be able to add an open number of cross-cutting concerns like logging, validations, audit, and security. Nesses casos, você pode confiar em um pipeline mediador (veja Padrão mediador) para oferecer um meio para esses comportamentos adicionais ou interesses transversais.In these cases, you can rely on a mediator pipeline (see Mediator pattern) to provide a means for these extra behaviors or cross-cutting concerns.

Um mediador é um objeto que encapsula o "como" desse processo: ele coordena a execução com base no estado, a maneira como um manipulador de comando é invocado ou a carga que você fornece ao manipulador.A mediator is an object that encapsulates the "how" of this process: it coordinates execution based on state, the way a command handler is invoked, or the payload you provide to the handler. Com um componente do mediador, você pode aplicar preocupações abrangentes de forma centralizada e transparente aplicando decoradores (ou comportamentos de pipeline desde o mediador 3).With a mediator component, you can apply cross-cutting concerns in a centralized and transparent way by applying decorators (or pipeline behaviors since MediatR 3). Para obter mais informações, consulte o Padrão decorador.For more information, see the Decorator pattern.

Os decoradores e comportamentos são semelhantes à AOP (Programação orientada a aspectos), aplicada somente a um pipeline de processo específico gerenciado pelo componente mediador.Decorators and behaviors are similar to Aspect Oriented Programming (AOP), only applied to a specific process pipeline managed by the mediator component. Os aspectos na AOP, que implementam interesses transversais, são aplicados com base em construtores de aspecto injetados em tempo de compilação ou com base na interceptação da chamada de objeto.Aspects in AOP that implement cross-cutting concerns are applied based on aspect weavers injected at compilation time or based on object call interception. Às vezes, essas duas abordagens de AOP típicas parecem funcionar "como mágica", porque não é fácil entender como a AOP faz seu trabalho.Both typical AOP approaches are sometimes said to work "like magic," because it is not easy to see how AOP does its work. Ao lidar com problemas sérios ou bugs, pode ser difícil depurar a AOP.When dealing with serious issues or bugs, AOP can be difficult to debug. Por outro lado, esses decoradores/comportamentos são explícitos e aplicados apenas no contexto do mediador, assim, a depuração fica muito mais fácil e previsível.On the other hand, these decorators/behaviors are explicit and applied only in the context of the mediator, so debugging is much more predictable and easy.

Por exemplo, no microserviço de ordenação eShopOnContainers, o tem uma implementação de dois comportamentos de exemplo, uma classe LogBehavior e uma classe ValidatorBehavior .For example, in the eShopOnContainers ordering microservice, has an implementation of two sample behaviors, a LogBehavior class and a ValidatorBehavior class. A implementação dos comportamentos é explicada na próxima seção mostrando como o eShopOnContainers usa os comportamentosdo mediador .The implementation of the behaviors is explained in the next section by showing how eShopOnContainers uses MediatR behaviors.

Usar filas de mensagens (fora do processo) no pipeline do comandoUse message queues (out-of-proc) in the command's pipeline

Outra opção é usar mensagens assíncronas com base em agentes ou filas de mensagens, conforme mostrado na Figura 7-26.Another choice is to use asynchronous messages based on brokers or message queues, as shown in Figure 7-26. Essa opção também pode ser combinada com o componente mediador imediatamente antes do manipulador de comandos.That option could also be combined with the mediator component right before the command handler.

Diagrama mostrando o Dataflow usando uma fila de mensagens de alta disponibilidade.

Figura 7-26.Figure 7-26. Usando filas de mensagens (fora do processo e comunicação entre processos) com comandos CQRSUsing message queues (out of the process and inter-process communication) with CQRS commands

O pipeline do comando também pode ser tratado por uma fila de mensagens de alta disponibilidade para entregar os comandos ao manipulador adequado.Command's pipeline can also be handled by a high availability message queue to deliver the commands to the appropriate handler. O uso de filas de mensagens para aceitar os comandos pode complicar ainda mais o pipeline do comando, pois você provavelmente precisará dividir o pipeline em dois processos conectados por meio da fila de mensagens externa.Using message queues to accept the commands can further complicate your command's pipeline, because you will probably need to split the pipeline into two processes connected through the external message queue. Ainda assim, isso deve ser usado se você precisa ter melhor escalabilidade e desempenho com base no sistema de mensagens assíncrono.Still, it should be used if you need to have improved scalability and performance based on asynchronous messaging. Considere que, no caso da Figura 7-26, o controlador apenas posta a mensagem de comando na fila e retorna.Consider that in the case of Figure 7-26, the controller just posts the command message into the queue and returns. Em seguida, o manipulador de comandos processa as mensagens em seu próprio ritmo.Then the command handlers process the messages at their own pace. Esse é um grande benefício das filas: a fila de mensagens pode agir como um buffer nos casos em que a hiperescalabilidade é necessária, como para estoques ou qualquer outro cenário com um alto volume de dados de entrada.That is a great benefit of queues: the message queue can act as a buffer in cases when hyper scalability is needed, such as for stocks or any other scenario with a high volume of ingress data.

No entanto, devido à natureza assíncrona das filas de mensagens, você precisa descobrir como se comunicar com o aplicativo cliente sobre o êxito ou a falha do processo do comando.However, because of the asynchronous nature of message queues, you need to figure out how to communicate with the client application about the success or failure of the command's process. Como regra, você nunca deve usar comandos "Fire and esqueça".As a rule, you should never use "fire and forget" commands. Todos os aplicativos de negócios precisam saber se um comando foi processado com êxito ou, pelo menos, validado e aceito.Every business application needs to know if a command was processed successfully, or at least validated and accepted.

Portanto, ser capaz de responder ao cliente depois de validar uma mensagem de comando que foi enviada para 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 após a execução da transação.Thus, being able to respond to the client after validating a command message that was submitted to an asynchronous queue adds complexity to your system, as compared to an in-process command process that returns the operation's result after running the transaction. Ao usar filas, talvez seja necessário retornar o resultado do processo do comando por meio de outras mensagens de resultado da operação, o que exigirá comunicação personalizada e componentes adicionais em seu sistema.Using queues, you might need to return the result of the command process through other operation result messages, which will require additional components and custom communication in your system.

Além disso, os comandos assíncronos são unidirecionais, o que, em muitos casos, pode não ser necessário, conforme explicado na seguinte discussão interessante entre Burtsev Alexey e Greg Young em uma conversa online:Additionally, async commands are one-way commands, which in many cases might not be needed, as is explained in the following interesting exchange between Burtsev Alexey and Greg Young in an online conversation:

[Burtsev Alexey ] eu encontrei muitos códigos em que as pessoas usam o manuseio de comandos assíncronos ou mensagens de comando unidirecionais sem qualquer motivo para fazer isso (eles não estão fazendo uma operação longa, eles não estão executando código assíncrono externo, eles nem mesmo limites entre aplicativos para usar o barramento de mensagem).[Burtsev Alexey] I find lots of code where people use async command handling or one-way command messaging without any reason to do so (they are not doing some long operation, they are not executing external async code, they do not even cross-application boundary to be using message bus). Por que eles introduzem essa complexidade desnecessária?Why do they introduce this unnecessary complexity? E, na verdade, eu não vi nenhum exemplo de código CQRS com manipuladores de comando de bloqueio até o momento, embora isso funcionaria muito bem na maioria dos casos.And actually, I haven't seen a CQRS code example with blocking command handlers so far, though it will work just fine in most cases.

[Greg Young ] [ ... ] um comando assíncrono não existe; na verdade, ele é outro evento.[Greg Young] [...] an asynchronous command doesn't exist; it's actually another event. Se eu precisar aceitar o que você me envia e gerar um evento se eu discordar, não será mais que você me informasse a fazer algo [ , não é um comando ] .If I must accept what you send me and raise an event if I disagree, it's no longer you telling me to do something [that is, it's not a command]. É você me informando que algo foi feito.It's you telling me something has been done. Parece que essa é apenas uma pequena diferença inicialmente, mas isso tem várias implicações.This seems like a slight difference at first, but it has many implications.

Os comandos assíncronos aumentam significativamente a complexidade de um sistema, porque não há nenhuma maneira simples de indicar falhas.Asynchronous commands greatly increase the complexity of a system, because there is no simple way to indicate failures. Portanto, os comandos assíncronos não são recomendados a não ser quando há requisitos de dimensionamento ou em casos especiais, ao comunicar os microsserviços internos por meio do sistema de mensagens.Therefore, asynchronous commands are not recommended other than when scaling requirements are needed or in special cases when communicating the internal microservices through messaging. Nesses casos, você deve projetar um sistema de relatórios e de recuperação separado para falhas.In those cases, you must design a separate reporting and recovery system for failures.

Na versão inicial do eShopOnContainers, foi decidido usar o processamento de comando síncrono, iniciado a partir de solicitações HTTP e controladas pelo padrão mediador.In the initial version of eShopOnContainers, it was decided to use synchronous command processing, started from HTTP requests and driven by the Mediator pattern. Isso permite retornar o êxito ou a falha do processo com facilidade, como na implementação de CreateOrderCommandHandler.That easily allows you to return the success or failure of the process, as in the CreateOrderCommandHandler implementation.

Em qualquer caso, isso deve ser uma decisão com base nos requisitos de negócios do seu aplicativo ou do microserviço.In any case, this should be a decision based on your application's or microservice's business requirements.

Implementar o pipeline de processo de comando com um padrão mediador (MediatR)Implement the command process pipeline with a mediator pattern (MediatR)

Como uma implementação de exemplo, este guia propõe o uso do pipeline em processo baseado no padrão Mediador para orientar a ingestão de comando e rotear comandos, na memória, para os manipuladores de comando corretos.As a sample implementation, this guide proposes using the in-process pipeline based on the Mediator pattern to drive command ingestion and route commands, in memory, to the right command handlers. O guia também propõe a aplicação de comportamentos para separar interesses transversais.The guide also proposes applying behaviors in order to separate cross-cutting concerns.

Para a implementação no .NET Core, há várias bibliotecas de software livre disponíveis que implementam o padrão Mediador.For implementation in .NET Core, there are multiple open-source libraries available that implement the Mediator pattern. A biblioteca usada neste guia é a MediatR, biblioteca de software livre criada por Jimmy Bogard, mas você pode usar outra abordagem.The library used in this guide is the MediatR open-source library (created by Jimmy Bogard), but you could use another approach. A MediatR é uma biblioteca pequena e simples que permite que você processe mensagens na memória como um comando, aplicando, ao mesmo tempo, decoradores ou comportamentos.MediatR is a small and simple library that allows you to process in-memory messages like a command, while applying decorators or behaviors.

O uso do padrão Mediador ajuda a reduzir o acoplamento e isolar as preocupações com o trabalho solicitado, ao conectar-se automaticamente com manipulador que executa esse trabalho — nesse caso, os manipuladores de comando.Using the Mediator pattern helps you to reduce coupling and to isolate the concerns of the requested work, while automatically connecting to the handler that performs that work—in this case, to command handlers.

Outra boa razão para usar o padrão Mediador foi explicada por Jimmy Bogard durante a revisão desse guia:Another good reason to use the Mediator pattern was explained by Jimmy Bogard when reviewing this guide:

Acho que vale a pena mencionar testes aqui – eles oferecem uma janela consistente e adequada sobre o comportamento do seu sistema.I think it might be worth mentioning testing here – it provides a nice consistent window into the behavior of your system. Solicitação-entrada, resposta. Descobrimos que o aspecto é bastante valioso na criação de testes com consistência.Request-in, response-out. We've found that aspect quite valuable in building consistently behaving tests.

Primeiro, vamos dar uma olhada em um controlador WebAPI de exemplo onde você realmente usaria o objeto mediador.First, let's look at a sample WebAPI controller where you actually would use the mediator object. Se você não estiver usando o objeto mediador, precisará injetar todas as dependências para esse controlador, coisas como um objeto logger e outros.If you weren't using the mediator object, you'd need to inject all the dependencies for that controller, things like a logger object and others. Portanto, o Construtor seria complicado.Therefore, the constructor would be complicated. Por outro lado, se você usasse o objeto mediador, o construtor do controlador poderia ser muito mais simples, com apenas algumas dependências em vez de muitas dependências, se você tivesse um por operação transversal, como no exemplo a seguir:On the other hand, if you use the mediator object, the constructor of your controller can be a lot simpler, with just a few dependencies instead of many dependencies if you had one per cross-cutting operation, as in the following example:

public class MyMicroserviceController : Controller
{
    public MyMicroserviceController(IMediator mediator,
                                    IMyMicroserviceQueries microserviceQueries)
    {
        // ...
    }
}

Veja que o mediador fornece um construtor de controlador da API Web simples e eficiente.You can see that the mediator provides a clean and lean Web API controller constructor. Além disso, dentro dos métodos do controlador, o código para enviar um comando para o objeto mediador tem quase uma linha:In addition, within the controller methods, the code to send a command to the mediator object is almost one line:

[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 idempotentesImplement idempotent Commands

Em eShopOnContainers, um exemplo mais avançado que o que foi visto está enviando um objeto CreateOrderCommand do microsserviço de pedidos.In eShopOnContainers, a more advanced example than the above is submitting a CreateOrderCommand object from the Ordering microservice. Mas como o processo comercial de ordenação é um pouco mais complexo e, em nosso caso, ele realmente começa no microserviço basket, essa ação de enviar o objeto CreateOrderCommand é executada 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.But since the Ordering business process is a bit more complex and, in our case, it actually starts in the Basket microservice, this action of submitting the CreateOrderCommand object is performed from an integration-event handler named UserCheckoutAcceptedIntegrationEventHandler instead of a simple WebAPI controller called from the client App as in the previous simpler example.

Mesmo assim, a ação de enviar o Comando para o MediatR é bem semelhante, conforme mostrado no código a seguir.Nevertheless, the action of submitting the Command to MediatR is pretty similar, as shown in the following code.

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, esse caso também é um pouco mais avançado, pois também estamos implementando comandos idempotentes.However, this case is also slightly more advanced because we're also implementing idempotent commands. O processo CreateOrderCommand deve ser idempotente, portanto, se a mesma mensagem vier duplicada pela rede, independentemente do motivo, como repetições, a mesma ordem de negócios será processada apenas uma vez.The CreateOrderCommand process should be idempotent, so if the same message comes duplicated through the network, because of any reason, like retries, the same business order will be processed just once.

Isso é implementado encapsulando o comando de negócios (neste caso, CreateOrderCommand) e inserindo-o em um IdentifiedCommand genérico, que é acompanhado por uma ID de cada mensagem proveniente da rede que precisa ser idempotente.This is implemented by wrapping the business command (in this case CreateOrderCommand) and embedding it into a generic IdentifiedCommand, which is tracked by an ID of every message coming through the network that has to be idempotent.

Veja no código abaixo que o IdentifiedCommand não passa de um DTO com uma ID, além do objeto do comando de negócios encapsulado.In the code below, you can see that the IdentifiedCommand is nothing more than a DTO with and ID plus the wrapped business command object.

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;
    }
}

Então, o CommandHandler do IdentifiedCommand chamado IdentifiedCommandHandler.cs vai basicamente verificar se a ID que está chegando como parte da mensagem já existe em uma tabela.Then the CommandHandler for the IdentifiedCommand named IdentifiedCommandHandler.cs will basically check if the ID coming as part of the message already exists in a table. Se ele já existir, esse comando não será processado novamente, portanto, ele se comporta como um comando idempotente.If it already exists, that command won't be processed again, so it behaves as an idempotent command. Esse código de infraestrutura é executado pela chamada de método _requestManager.ExistAsync abaixo.That infrastructure code is performed by the _requestManager.ExistAsync method call below.

// 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 embeded 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 atua como um envelope de comando de negócios, quando o comando comercial precisa ser processado porque não é uma ID repetida, ele pega o comando comercial interno e o reenvia para mediador, como na última parte do código mostrado acima durante _mediator.Send(message.Command) a execução, do IdentifiedCommandHandler.cs.Since the IdentifiedCommand acts like a business command's envelope, when the business command needs to be processed because it is not a repeated ID, then it takes that inner business command and resubmits it to Mediator, as in the last part of the code shown above when running _mediator.Send(message.Command), from the IdentifiedCommandHandler.cs.

Ao fazer isso, ele vinculará e executará o manipulador de comandos de negócios, nesse caso, o CreateOrderCommandHandler, que está executando transações em relação ao banco de dados de ordenação, conforme mostrado no código a seguir.When doing that, it will link and run the business command handler, in this case, the CreateOrderCommandHandler, which is running transactions against the Ordering database, as shown in the following code.

// 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 MediatRRegister the types used by MediatR

Para que o MediatR tome ciência de suas classes de manipulador de comando, é necessário registrar as classes de mediador e as classes de manipulador de comandos em seu contêiner de IoC.In order for MediatR to be aware of your command handler classes, you need to register the mediator classes and the command handler classes in your IoC container. Por padrão, o MediatR usa o Autofac como contêiner de IoC, mas você também pode usar o contêiner de IoC interno do ASP.NET Core ou qualquer outro contêiner compatível com o MediatR.By default, MediatR uses Autofac as the IoC container, but you can also use the built-in ASP.NET Core IoC container or any other container supported by MediatR.

O código a seguir mostra como registrar tipos e comandos do mediador ao usar módulos Autofac.The following code shows how to register Mediator's types and commands when using Autofac modules.

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
        //...
    }
}

É aí que "a mágica acontece" com o mediador.This is where "the magic happens" with MediatR.

Como cada manipulador de comandos implementa a IRequestHandler<T> interface genérica, quando você registra os assemblies usando o RegisteredAssemblyTypes método, todos os tipos marcados como IRequestHandler também são registrados com seus Commands .As each command handler implements the generic IRequestHandler<T> interface, when you register the assemblies using RegisteredAssemblyTypes method all the types marked as IRequestHandler also gets registered with their Commands. Por exemplo:For example:

public class CreateOrderCommandHandler
  : IRequestHandler<CreateOrderCommand, bool>
{

Esse é o código que correlaciona comandos com manipuladores de comandos.That is the code that correlates commands with command handlers. O manipulador é apenas uma classe simples, mas ele herda de RequestHandler<T>, em que T é o tipo de comando, enquanto o MediatR garante que ele seja invocado com o conteúdo correto.The handler is just a simple class, but it inherits from RequestHandler<T>, where T is the command type, and MediatR makes sure it is invoked with the correct payload (the command).

Aplicar questões abrangentes ao processar comandos com os Comportamentos no MediatRApply cross-cutting concerns when processing commands with the Behaviors in MediatR

Há mais um assunto para discutir: a capacidade de aplicar interesses transversais ao pipeline mediador.There is one more thing: being able to apply cross-cutting concerns to the mediator pipeline. Você também pode ver, no final do código do módulo de registro do Autofac, como ele registra um tipo de comportamento, especificamente uma classe LoggingBehavior personalizada e uma classe ValidatorBehavior.You can also see at the end of the Autofac registration module code how it registers a behavior type, specifically, a custom LoggingBehavior class and a ValidatorBehavior class. Mas você também pode adicionar outros comportamentos personalizados.But you could add other custom behaviors, too.

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, que registra informações sobre o manipulador de comando que está sendo executado e se ele foi bem-sucedido ou não, pode ser implementada como no código a seguir.That LoggingBehavior class can be implemented as the following code, which logs information about the command handler being executed and whether it was successful or not.

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;
    }
}

Basta implementar essa classe de comportamento e registrar o pipeline com ela (no MediatorModule acima) e todos os comandos processados por meio do MediatR registrarão em log as informações sobre a execução.Just by implementing this behavior class and by registering it in the pipeline (in the MediatorModule above), all the commands processed through MediatR will be logging information about the execution.

O microsserviço de pedidos 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:The eShopOnContainers ordering microservice also applies a second behavior for basic validations, the ValidatorBehavior class that relies on the FluentValidation library, as shown in the following code:

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 é gerar uma exceção se a validação falhar, mas você também poderá retornar um objeto de resultado, que contém o resultado do comando se ele tiver êxito ou as mensagens de validação, caso não tenha sido.Here the behavior is raising an exception if validation fails, but you could also return a result object, containing the command result if it succeeded or the validation messages in case it didn't. Isso provavelmente tornaria mais fácil exibir os resultados da validação para o usuário.This would probably make it easier to display validation results to the user.

Em seguida, com base na biblioteca FluentValidation , você criaria a validação para os dados passados com CreateOrderCommand, como no código a seguir:Then, based on the FluentValidation library, you would create validation for the data passed with CreateOrderCommand, as in the following code:

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.You could create additional validations. Essa é uma maneira muito eficiente e elegante de se implementar validações de comando.This is a very clean and elegant way to implement your command validations.

De maneira semelhante, você pode implementar outros comportamentos para aspectos adicionais ou interesses transversais que você deseja aplicar aos comandos ao manipulá-los.In a similar way, you could implement other behaviors for additional aspects or cross-cutting concerns that you want to apply to commands when handling them.

Recursos adicionaisAdditional resources

O padrão mediadorThe mediator pattern
O padrão decoradorThe decorator pattern
MediatR (Jimmy Bogard)MediatR (Jimmy Bogard)
Validação fluenteFluent validation