Реализация прикладного уровня для микрослужб с помощью веб-APIImplement the microservice application layer using the Web API

Внедрение объектов инфраструктуры в прикладной уровень с помощью внедрения зависимостейUse Dependency Injection to inject infrastructure objects into your application layer

Как упоминалось ранее, прикладной уровень может быть реализован в рамках создаваемого артефакта (сборки), например проекта веб-интерфейса API или проекта веб-приложения 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. При создании микрослужбы с помощью ASP.NET Core прикладным уровнем обычно является библиотека веб-интерфейсов API.In the case of a microservice built with ASP.NET Core, the application layer will usually be your Web API library. Чтобы отделить ту часть, которая берется из ASP.NET Core (инфраструктуру и контроллеры), от пользовательского кода прикладного уровня, можно поместить прикладной уровень в отдельную библиотеку классов, хотя это необязательно.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.

Например, код прикладного уровня для микрослужбы размещения заказов реализуется непосредственно в рамках проекта Ordering.API (проекта веб-интерфейса API ASP.NET Core), как показано на рис. 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.

Представление обозревателя решений для микрослужбы Ordering.API, показывающее вложенные папки в папке приложения: Behaviors, Commands, DomainEventHandlers, IntegrationEvents, Models, Queries и Validations.

Рис. 7-23.Figure 7-23. Прикладной уровень в проекте веб-интерфейса API ASP.NET Core Ordering.APIThe application layer in the Ordering.API ASP.NET Core Web API project

ASP.NET Core содержит простой встроенный контейнер IoC (представленный интерфейсом IServiceProvider), поддерживающий внедрение через конструктор по умолчанию, а ASP.NET предоставляет посредством внедрения зависимостей определенные службы.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. В ASP.NET Core под термином служба понимаются любые зарегистрированные типы, которые будут внедряться посредством внедрения зависимостей.ASP.NET Core uses the term service for any of the types you register that will be injected through DI. Настроить службы встроенного контейнера можно в методе ConfigureServices в классе Startup вашего приложения.You configure the built-in container's services in the ConfigureServices method in your application's Startup class. Зависимости реализуются в службах, которые требуются типу и которые вы регистрируете в контейнере IoC.Your dependencies are implemented in the services that a type needs and that you register in the IoC container.

Как правило, необходимо внедрить зависимости, реализующие объекты инфраструктуры.Typically, you want to inject dependencies that implement infrastructure objects. Типичной внедряемой зависимостью является репозиторий.A very typical dependency to inject is a repository. Однако можно внедрять и любые другие имеющиеся зависимости инфраструктуры.But you could inject any other infrastructure dependency that you may have. Чтобы упростить реализацию, можно напрямую внедрить объект на основе шаблона "Единица работы" (объект EF DbContext), так как DBContext также является реализацией сохраняемых объектов инфраструктуры.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.

В приведенном ниже примере показано, как платформа .NET Core внедряет необходимые объекты репозитория посредством конструктора.In the following example, you can see how .NET Core is injecting the required repository objects through the constructor. Класс представляет собой обработчик команд, о котором мы поговорим в следующем разделе.The class is a command handler, which we will cover in the next section.

public class CreateOrderCommandHandler
    : IAsyncRequestHandler<CreateOrderCommand, bool>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IIdentityService _identityService;
    private readonly IMediator _mediator;

    // Using DI to inject infrastructure persistence Repositories
    public CreateOrderCommandHandler(IMediator mediator,
                                     IOrderRepository orderRepository,
                                     IIdentityService identityService)
    {
        _orderRepository = orderRepository ??
                          throw new ArgumentNullException(nameof(orderRepository));
        _identityService = identityService ??
                          throw new ArgumentNullException(nameof(identityService));
        _mediator = mediator ??
                                 throw new ArgumentNullException(nameof(mediator));
    }

    public async Task<bool> Handle(CreateOrderCommand message)
    {
        // Create the Order AggregateRoot
        // 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, 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);
        }

        _orderRepository.Add(order);

        return await _orderRepository.UnitOfWork
            .SaveEntitiesAsync();
    }
}

Этот класс использует внедренные репозитории для выполнения транзакции и сохранения изменений состояния.The class uses the injected repositories to execute the transaction and persist the state changes. Не имеет значения, является ли класс обработчиком команд, методом контроллера веб-интерфейса API ASP.NET Core или службой приложения 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. В любом случае он представляет собой простой класс, который использует репозитории, сущности домена и другие средства координации приложения подобно тому, как это делает обработчик команд.It is ultimately a simple class that uses repositories, domain entities, and other application coordination in a fashion similar to a command handler. Внедрение зависимостей работает одинаково для всех перечисленных выше классов, как в этом примере, в котором внедрение зависимостей основано на конструкторе.Dependency Injection works the same way for all the mentioned classes, as in the example using DI based on the constructor.

Регистрация типов и интерфейсов или абстракций для реализации зависимостейRegister the dependency implementation types and interfaces or abstractions

Прежде чем использовать объекты, внедренные посредством конструкторов, необходимо знать, где следует зарегистрировать интерфейсы и классы, которые предоставляют объекты, внедренные в приложение с помощью внедрения зависимостей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. (например, в случае внедрения зависимостей на основе конструктора, показанного выше).(Like DI based on the constructor, as shown previously.)

Использование встроенного контейнера IoC, предоставляемого платформой ASP.NET CoreUse the built-in IoC container provided by ASP.NET Core

При использовании встроенного контейнера IoC, предоставляемого платформой ASP.NET Core, типы, которые нужно внедрить, регистрируются в методе ConfigureServices в файле Startup.cs, как в следующем коде: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>();
}

Чаще всего типы регистрируются в контейнере IoC парами, состоящими из интерфейса и связанного с ним класса реализации.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. Затем при запросе объекта из контейнера IoC посредством любого конструктора вы запрашиваете объект определенного типа интерфейса.Then when you request an object from the IoC container through any constructor, you request an object of a certain type of interface. Например, в предыдущем примере в последней строке указано, что когда любой из конструкторов имеет зависимость от IMyCustomRepository (интерфейса или абстракции), контейнер IoC внедряет экземпляр класса реализации 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.

Автоматическая регистрация типов с помощью библиотеки ScrutorUse the Scrutor library for automatic types registration

При использовании внедрения зависимостей в .NET Core может потребоваться проверить сборку и автоматически зарегистрировать ее типы в соответствии с соглашением.When using DI in .NET Core, you might want to be able to scan an assembly and automatically register its types by convention. Эта возможность в настоящее время недоступна в ASP.NET Core.This feature is not currently available in ASP.NET Core. Однако для этого можно использовать библиотеку Scrutor.However, you can use the Scrutor library for that. Такой подход удобен в случае, если имеются десятки типов, которые необходимо зарегистрировать в контейнере IoC.This approach is convenient when you have dozens of types that need to be registered in your IoC container.

Дополнительные ресурсыAdditional resources

Использование Autofac в качестве контейнера IoCUse Autofac as an IoC container

Вы также можете использовать дополнительные контейнеры IoC и подключить их к конвейеру ASP.NET Core, как в случае с микрослужбой размещения заказов в eShopOnContainers, которая использует 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. При использовании Autofac типы, как правило, регистрируются посредством модулей, что позволяет разделить типы регистрации на несколько файлов в зависимости от местонахождения типов, так же как типы приложения распределяются по нескольким библиотекам классов.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.

Например, ниже приведен модуль приложения Autofac для проекта веб-интерфейса API Ordering.API с типами, которые потребуется внедрить.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();
   }
}

Autofac также может сканировать сборки и регистрировать типы в соответствии с соглашениями об именовании.Autofac also has a feature to scan assemblies and register types by name conventions.

Принципы и процесс регистрации очень похожи на регистрацию типов с помощью встроенного контейнера IoC ASP.NET Core, но синтаксис при использовании Autofac немного иной.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.

В примере кода абстракция IOrderRepository регистрируется вместе с классом реализации OrderRepository.In the example code, the abstraction IOrderRepository is registered along with the implementation class OrderRepository. Это означает, что каждый раз, когда в конструкторе объявляется зависимость посредством абстракции или интерфейса IOrderRepository, контейнер IoC внедряет экземпляр класса 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.

Тип области экземпляра определяет то, как экземпляр используется совместно при запросах к одной и той же службе или зависимости.The instance scope type determines how an instance is shared between requests for the same service or dependency. При запросе зависимости контейнер IoC может вернуть следующее:When a request is made for a dependency, the IoC container can return the following:

  • один экземпляр для каждой области времени существования (в контейнере IoC ASP.NET Core такой экземпляр называется экземпляром с заданной областью);A single instance per lifetime scope (referred to in the ASP.NET Core IoC container as scoped).

  • новый экземпляр для каждой зависимости (в контейнере IoC ASP.NET Core такой экземпляр называется временным);A new instance per dependency (referred to in the ASP.NET Core IoC container as transient).

  • один общий экземпляр для всех объектов, использующих контейнер IoC (в контейнере IoC ASP.NET Core такой экземпляр называется единичным).A single instance shared across all objects using the IoC container (referred to in the ASP.NET Core IoC container as singleton).

Дополнительные ресурсыAdditional resources

Реализация шаблонов команд и обработчиков командImplement the Command and Command Handler patterns

В примере внедрения зависимостей через конструктор в предыдущем разделе контейнер IoC внедрял репозитории через конструктор в классе.In the DI-through-constructor example shown in the previous section, the IoC container was injecting repositories through a constructor in a class. Но куда именно они внедрялись?But exactly where were they injected? В простом веб-API (например, микрослужбе каталога в eShopOnContainers) они внедряются на уровне контроллеров MVC в конструкторе контроллера как часть конвейера запросов в 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. Однако в коде, приведенном в начале этого раздела (класс CreateOrderCommandHandler из службы Ordering.API в eShopOnContainers), внедрение зависимостей производится через конструктор определенного обработчика команд.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. Давайте рассмотрим, что такое обработчик команд и зачем его использовать.Let us explain what a command handler is and why you would want to use it.

Шаблон команды, по сути, связан с шаблоном CQRS, который был представлен ранее в этом руководстве.The Command pattern is intrinsically related to the CQRS pattern that was introduced earlier in this guide. CQRS имеет два аспекта.CQRS has two sides. Первый аспект — это запросы, причем используются упрощенные запросы на основе микро-ORM Dapper, который был рассмотрен ранее.The first area is queries, using simplified queries with the Dapper micro ORM, which was explained previously. Второй аспект — это команды, которые являются отправной точкой для транзакций, и канал входных данных извне службы.The second area is commands, which are the starting point for transactions, and the input channel from outside the service.

Как показано на рис. 7-24, шаблон основан на принятии команд со стороны клиента, их обработке на основе правил модели предметной области и, наконец, сохранении состояний с помощью транзакций.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.

Представление высокого уровня стороны записи в CQRS: приложение пользовательского интерфейса отправляет команду через API, которая получает CommandHandler, зависящий от модели предметной области и инфраструктуры для обновления базы данных.

Рис. 7-24.Figure 7-24. Общее представление команд или "уровня транзакций" в шаблоне CQRSHigh-level view of the commands or “transactional side” in a CQRS pattern

Класс командThe command class

Команда — это запрос к системе на выполнение действия, которое изменяет состояние системы.A command is a request for the system to perform an action that changes the state of the system. Команды являются императивными и должны обрабатываться только один раз.Commands are imperative, and should be processed just once.

Так как команды являются императивами, в их именах обычно есть глагол в повелительном наклонении (например, create или update) и может присутствовать тип агрегата, например 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. В отличие от события, команда не связана с фактом в прошлом. Это всего лишь запрос, который может быть отклонен.Unlike an event, a command is not a fact from the past; it is only a request, and thus may be refused.

Команды могут поступать из пользовательского интерфейса в результате инициации запроса пользователем или из диспетчера процессов, когда он предписывает агрегату выполнить действие.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.

Важной особенностью команды является то, что она должна обрабатываться только один раз одним получателем.An important characteristic of a command is that it should be processed just once by a single receiver. Связано это с тем, что команда представляет собой одно действие или транзакцию, которую нужно выполнить в приложении.This is because a command is a single action or transaction you want to perform in the application. Например, одну и ту же команду создания заказа не следует обрабатывать несколько раз.For example, the same order creation command should not be processed more than once. Это важное различие между командами и событиями.This is an important difference between commands and events. События могут обрабатываться несколько раз, так как одно событие может представлять интерес для множества систем или микрослужб.Events may be processed multiple times, because many systems or microservices might be interested in the event.

Кроме того, важно, чтобы команда обрабатывалась только одни раз, если она не является идемпотентной.In addition, it is important that a command be processed only once in case the command is not idempotent. Команда является идемпотентной, если она может выполняться несколько раз с одинаковым результатом либо из-за самого ее характера, либо из-за способа обработки команды системой.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.

Команды имеет смысл делать идемпотентными, если этого требуют бизнес-правила и инварианты предметной области.It is a good practice to make your commands and updates idempotent when it makes sense under your domain’s business rules and invariants. Продолжая приведенный выше пример, если по какой-либо причине (логика повтора, взлом и т. д.) одна и та же команда CreateOrder поступает в систему несколько раз, необходимо иметь возможность определить такую ситуацию и предотвратить создание нескольких заказов.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. Для этого в операции необходимо включить какой-либо идентификатор, чтобы определять, были ли команда или обновление уже обработаны.To do so, you need to attach some kind of identity in the operations and identify whether the command or update was already processed.

Команда отправляется одному получателю. Она не публикуется.You send a command to a single receiver; you do not publish a command. Публикуются события, сообщающие некий факт — что произошло что-то, что может представлять интерес для получателей событий.Publishing is for events that state a fact—that something has happened and might be interesting for event receivers. Издателя событий не интересует, какие приемники получат событие и что они будут с ним делать.In the case of events, the publisher has no concerns about which receivers get the event or what they do it. Однако с событиями интеграции или предметной области дело обстоит иначе, о чем уже говорилось в предыдущих разделах.But domain or integration events are a different story already introduced in previous sections.

Команда реализуется с помощью класса, который содержит поля данных или коллекции со всеми сведениями, необходимыми для выполнения этой команды.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. Команда — это объект передачи данных (DTO) особого типа, предназначенный специально для запроса изменений или транзакций.A command is a special kind of Data Transfer Object (DTO), one that is specifically used to request changes or transactions. Сама по себе команда основана только на тех сведениях, которые необходимы для ее обработки, и ни на чем больше.The command itself is based on exactly the information that is needed for processing the command, and nothing more.

В приведенном ниже примере показан упрощенный класс CreateOrderCommand.The following example shows the simplified CreateOrderCommand class. Это неизменяемая команда, которая используется в микрослужбе размещения заказов в eShopOnContainers.This is an immutable command that is used in the ordering microservice in eShopOnContainers.

// DDD and CQRS patterns comment
// Note that we recommend that you implement immutable commands
// In this case, immutability is achieved by having all the setters as private
// plus being able to update the data just once, when creating the object
// through the 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://msdn.microsoft.com/library/bb383979.aspx
[DataContract]
public class CreateOrderCommand
    :IAsyncRequest<bool>
{
    [DataMember]
    private readonly List<OrderItemDTO> _orderItems;
    [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 city,
        string street,
        string state, string country, string zipcode,
        string cardNumber, string cardHolderName, DateTime cardExpiration,
        string cardSecurityNumber, int cardTypeId) : this()
    {
        _orderItems = MapToOrderItems(basketItems);
        City = city;
        Street = street;
        State = state;
        Country = country;
        ZipCode = zipcode;
        CardNumber = cardNumber;
        CardHolderName = cardHolderName;
        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; }
    }
}

По существу, класс команды содержит все данные, необходимые для выполнения бизнес-транзакции с помощью объектов модели предметной области.Basically, the command class contains all the data you need for performing a business transaction by using the domain model objects. Таким образом, команды — это попросту структуры данных, которые содержат доступные только для чтения данные, но не алгоритмы.Thus, commands are simply data structures that contain read-only data, and no behavior. Имя команды указывает на ее назначение.The command’s name indicates its purpose. Во многих языках, например в C#, команды представлены классами, но они не являются настоящими классами в объектно-ориентированном смысле.In many languages like C#, commands are represented as classes, but they are not true classes in the real object-oriented sense.

Дополнительной характеристикой команд является неизменяемость, так как предполагается, что они обрабатываются непосредственно моделью предметной области.As an additional characteristic, commands are immutable, because the expected usage is that they are processed directly by the domain model. Они не должны изменяться в течение предполагаемого времени существования.They do not need to change during their projected lifetime. В классе C# неизменяемость может обеспечиваться благодаря отсутствию методов задания или других методов, которые изменяют внутреннее состояние.In a C# class, immutability can be achieved by not having any setters or other methods that change internal state.

Если предполагается, что команды будут проходить процесс сериализации/десериализации, свойства должны иметь закрытый метод задания и атрибут [DataMember] (или [JsonProperty]), в противном случае десериализатор не сможет восстановить объект в месте назначения с необходимыми значениями.Bear in mind that if you intend or expect commands will be going through a serializing/deserializing process, the properties must have private setter, and the [DataMember] (or [JsonProperty]) attribute, otherwise the deserializer will not be able to reconstruct the object at destination with the required values.

Например, в плане данных класс команды для создания заказа может быть аналогичен создаваемому заказу, однако, вероятно, требуемые атрибуты могут различаться.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. Например, команда CreateOrderCommand не включает в себя идентификатор заказа, так как заказ еще не создан.For instance, CreateOrderCommand does not have an order ID, because the order has not been created yet.

Многие классы команд могут быть простыми и требуют лишь несколько полей, связанных с изменяемым состоянием.Many command classes can be simple, requiring only a few fields about some state that needs to be changed. Например, это верно в случае изменения состояния заказа с "обрабатывается" на "оплачен" или "отправлен" с помощью команды наподобие следующей: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
    :IAsyncRequest<bool>
{
    [DataMember]
    public string Status { get; private set; }

    [DataMember]
    public string OrderId { get; private set; }

    [DataMember]
    public string BuyerIdentityGuid { get; private set; }
}

Некоторые разработчики разделяют объекты запросов пользовательского интерфейса и объекты DTO команд, но это всего лишь вопрос предпочтений.Some developers make their UI request objects separate from their command DTOs, but that is just a matter of preference. Такое разделение требует значительных усилий, а преимущества невелики, причем объекты очень схожи по сути.It is a tedious separation with not much added value, and the objects are almost exactly the same shape. Например, в eShopOnContainers некоторые команды поступают непосредственно со стороны клиента.For instance, in eShopOnContainers, some commands come directly from the client side.

Класс обработчика командThe Command Handler class

Для каждой команды следует реализовать класс обработчика команд.You should implement a specific command handler class for each command. Этого требует шаблон, и именно в этом классе будут использоваться объект команды, объекты предметной области и объекты репозиториев инфраструктуры.That is how the pattern works, and it is where you will use the command object, the domain objects, and the infrastructure repository objects. Обработчик команд — это, по сути, центральный элемент прикладного уровня в рамках CQRS и DDD.The command handler is in fact the heart of the application layer in terms of CQRS and DDD. Однако вся логика предметной области должна содержаться в классах предметной области — корневых объектах агрегатов (корневых сущностях), дочерних сущностях или службах предметной области, но не в обработчике команд, который представляет собой класс прикладного уровня.However, all the domain logic should be contained within 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.

Класс обработчика команд предоставляет надежную основу для реализации принципа единой ответственности (SRP), упомянутого в предыдущем разделе.The command handler class offers a strong stepping stone in the way to achieve the Single Responsibility Principle (SRP) mentioned in a previous section.

Обработчик команд принимает команду и получает результат из используемого агрегата.A command handler receives a command and obtains a result from the aggregate that is used. Результатом должно быть успешное выполнение команды или исключение.The result should be either successful execution of the command, or an exception. В случае исключения состояние системы не должно меняться.In the case of an exception, the system state should be unchanged.

Обработчик команд обычно выполняет следующие действия:The command handler usually takes the following steps:

  • получает объект команды, например DTO (от медиатора или другого объекта инфраструктуры);It receives the command object, like a DTO (from the mediator or other infrastructure object).

  • проверяет допустимость команды (если она не была проверена медиатором);It validates that the command is valid (if not validated by the mediator).

  • создает экземпляр корневой сущности агрегата, являющийся целевым для текущей команды;It instantiates the aggregate root instance that is the target of the current command.

  • выполняет метод экземпляра корневой сущности агрегата, получая необходимые данные из команды;It executes the method on the aggregate root instance, getting the required data from the command.

  • сохраняет новое состояние агрегата в связанной с ним базе данных.It persists the new state of the aggregate to its related database. Эта последняя операция и является транзакцией.This last operation is the actual transaction.

Как правило, обработчик команд работает с одним агрегатом, определяемым корневым объектом агрегата (корневой сущностью).Typically, a command handler deals with a single aggregate driven by its aggregate root (root entity). Если принимаемая команда должна влиять на несколько агрегатов, можно использовать события предметной области для распространения состояний или действий в нескольких агрегатах.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.

Важным моментом является то, что при обработке команды вся логика предметной области должна находиться внутри модели предметной области (агрегатов), причем она должна быть полностью инкапсулирована и готова для модульного тестирования.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. Обработчик команд служит лишь для того, чтобы получить модель предметной области из базы данных и (на последнем этапе) сообщить уровню инфраструктуры (репозиториям) о необходимости сохранить изменения в случае изменения модели.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. Преимуществом такого подхода является возможность рефакторинга логики предметной области в рамках изолированной, полностью инкапсулированной поведенческой модели предметной области без изменения кода на прикладном уровне или уровне инфраструктуры, то есть на связующем уровне (обработчики команд, веб-интерфейс API, репозитории и т. д.).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.).

Если обработчики команд становятся слишком сложными, содержащими слишком много логики, это может быть признаком плохого кода.When command handlers get complex, with too much logic, that can be a code smell. Проверьте их и, если найдете логику предметной области, выполните рефакторинг кода, чтобы переместить эту логику в методы объектов предметной области (корневая сущность агрегата и дочерняя сущность).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).

В приведенном ниже коде в качестве примера класса обработчика команд показан тот же класс CreateOrderCommandHandler, который вы уже видели в начале этой главы.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. В этом случае мы хотим привлечь внимание к методу Handle и операциям с объектами и агрегатами модели предметной области.In this case, we want to highlight the Handle method and the operations with the domain model objects/aggregates.

public class CreateOrderCommandHandler
    : IAsyncRequestHandler<CreateOrderCommand, bool>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IIdentityService _identityService;
    private readonly IMediator _mediator;

    // Using DI to inject infrastructure persistence Repositories
    public CreateOrderCommandHandler(IMediator mediator,
                                     IOrderRepository orderRepository,
                                     IIdentityService identityService)
    {
        _orderRepository = orderRepository ??
                          throw new ArgumentNullException(nameof(orderRepository));
        _identityService = identityService ??
                          throw new ArgumentNullException(nameof(identityService));
        _mediator = mediator ??
                                 throw new ArgumentNullException(nameof(mediator));
    }

    public async Task<bool> Handle(CreateOrderCommand message)
    {
        // Create the Order AggregateRoot
        // 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, 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);
        }

        _orderRepository.Add(order);

        return await _orderRepository.UnitOfWork
            .SaveEntitiesAsync();
    }
}

Обработчик команд должен выполнять следующие дополнительные действия:These are additional steps a command handler should take:

  • использовать данные команды для взаимодействия с методами и поведением корневой сущности агрегата;Use the command’s data to operate with the aggregate root’s methods and behavior.

  • вызывать события предметной области внутри объектов предметной области во время выполнения транзакции прозрачным с точки зрения обработчика команд образом;Internally within the domain objects, raise domain events while the transaction is executed, but that is transparent from a command handler point of view.

  • в случае успешного результата операции агрегата вызывать события интеграции после завершения транзакции.If the aggregate’s operation result is successful and after the transaction is finished, raise integration events. (Эти события могут также вызываться классами инфраструктуры, такими как репозитории.)(These might also be raised by infrastructure classes like repositories.)

Дополнительные ресурсыAdditional resources

Конвейер обработки команд: активация обработчика командThe Command process pipeline: how to trigger a command handler

Следующий вопрос заключается в том, как вызывается обработчик команд.The next question is how to invoke a command handler. Его можно вызывать из каждого связанного контроллера ASP.NET Core.You could manually call it from each related ASP.NET Core controller. Однако такой подход требует слишком большого количества связей и не идеален.However, that approach would be too coupled and is not ideal.

Другие два варианта, которые рекомендуются, представлены ниже:The other two main options, which are the recommended options, are:

  • посредством артефакта шаблона медиатора в памяти;Through an in-memory Mediator pattern artifact.

  • с помощью асинхронной очереди сообщений между контроллерами и обработчиками.With an asynchronous message queue, in between controllers and handlers.

Использование шаблона медиатора (в памяти) в конвейере командUse the Mediator pattern (in-memory) in the command pipeline

Как показано на рис. 7-25, в рамках подхода на основе CQRS используется интеллектуальный медиатор, похожий на шину в памяти, который может осуществлять перенаправление в нужный обработчик команд в соответствии с типом полученной команды или объекта DTO.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. Одиночные черные стрелки между компонентами представляют зависимости между объектами (которые часто внедряются посредством внедрения зависимостей) и соответствующие взаимодействия.The single black arrows between components represent the dependencies between objects (in many cases, injected through DI) with their related interactions.

Детальное изображение фрагмента предыдущего рисунка: контроллер ASP.NET Core отправляет команду в конвейер команд MediatR, чтобы они попали в соответствующий обработчик.

Рис. 7-25.Figure 7-25. Использование шаблона медиатора в процессе обработки в отдельной микрослужбе CQRSUsing the Mediator pattern in process in a single CQRS microservice

Основанием для применения шаблона медиатора является то, что в корпоративных приложениях обработка запросов может становиться сложной.The reason that using the Mediator pattern makes sense is that in enterprise applications, the processing requests can get complicated. Вам может требоваться добавить сквозную функциональность, например ведения журнала, проверки, аудит и безопасность.You want to be able to add an open number of cross-cutting concerns like logging, validations, audit, and security. В таком случае вы можете применить конвейер медиатора (см. статью Шаблон медиатора) в качестве средства для реализации дополнительной сквозной функциональности или поведения.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.

Медиатор — это объект, который инкапсулирует методы выполнения этого процесса: он координирует выполнение на основе состояния, способы вызова обработчика команд и полезные данные, предоставляемые ему.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. С помощью медиатора вы можете применять сквозную функциональность централизованным и прозрачным образом, применяя декораторы (или расширения функциональности конвейера начиная с MediatR 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). Дополнительную информацию см. в статье Шаблон декоратора.For more information, see the Decorator pattern.

Декораторы и расширения функциональности похожи на аспектно-ориентированное программирование (АОП), но применяются к определенному конвейеру обработки, управляемому медиатором.Decorators and behaviors are similar to Aspect Oriented Programming (AOP), only applied to a specific process pipeline managed by the mediator component. Аспекты в АОП, которые реализуют сквозную функциональность, применяются на основе средств внедрения аспектов, внедряемых во время компиляции или с помощью перехвата вызовов объектов.Aspects in AOP that implement cross-cutting concerns are applied based on aspect weavers injected at compilation time or based on object call interception. Иногда говорят, что оба подхода АОП работают "волшебным образом", так как их применение трудно проследить.Both typical AOP approaches are sometimes said to work "like magic," because it is not easy to see how AOP does its work. В случае серьезных проблем или ошибок отладка в АОП может вызывать трудности.When dealing with serious issues or bugs, AOP can be difficult to debug. С другой стороны, эти декораторы и расширения функциональности являются явными и применяются только в контексте медиатора, поэтому отладка является гораздо более предсказуемой и легкой.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.

Например, в микрослужбе размещения заказов eShopOnContainers реализованы два образца расширений функциональности: класс LogBehavior и класс ValidatorBehavior.For example, in the eShopOnContainers ordering microservice, we implemented two sample behaviors, a LogBehavior class and a ValidatorBehavior class. Реализация расширений функциональности рассмотрена в следующем разделе. В нем показано, как в eShopOnContainers используются расширения функциональности MediatR 3.The implementation of the behaviors is explained in the next section by showing how eShopOnContainers uses MediatR 3 behaviors.

Использование очередей сообщений (внепроцессных) в конвейере командUse message queues (out-of-proc) in the command’s pipeline

Еще один вариант — это использование асинхронных сообщений на основе брокеров или очередей сообщений, как показано на рис. 7-26.Another choice is to use asynchronous messages based on brokers or message queues, as shown in Figure 7-26. Его можно использовать в сочетании с компонентом медиатора непосредственно перед обработчиком команд.That option could also be combined with the mediator component right before the command handler.

Конвейер команды также может обрабатываться очередью сообщений высокой доступности для доставки команд соответствующему обработчику.

Рис. 7-26.Figure 7-26. Использование очередей сообщений (внепроцессное и внутрипроцессное взаимодействие) с командами CQRSUsing message queues (out of process and inter-process communication) with CQRS commands

Использование очередей сообщений для принятия команд может еще более усложнить конвейер команд, так как вам может потребоваться разделить его на два процесса, связанных посредством внешней очереди сообщений.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. Однако его следует использовать, если вам нужно повысить масштабируемость и производительность с помощью асинхронных сообщений.Still, it should be used if you need to have improved scalability and performance based on asynchronous messaging. Представьте, что в ситуации, продемонстрированной на рис. 7-26, контроллер просто отправляет сообщение команды в очередь и возвращает управление.Consider that in the case of Figure 7-26, the controller just posts the command message into the queue and returns. В этом случае обработчики команд обрабатывают сообщения в удобном для себя темпе.Then the command handlers process the messages at their own pace. Это важное преимущество очередей: очередь сообщений может служить буфером в случае, когда требуется высочайшая масштабируемость, например в сценариях с большим объемом входящих данных.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.

Однако из-за асинхронного характера очередей сообщений необходимо продумать способ, который позволит сообщать клиентскому приложению об успешном или неудачном выполнении процесса команды.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. Как правило, не следует использовать команды, выполняющиеся в автономном режиме.As a rule, you should never use “fire and forget” commands. Каждому бизнес-приложению требуется знать, была ли команда обработана успешно или по крайней мере была ли она проверена и принята.Every business application needs to know if a command was processed successfully, or at least validated and accepted.

Таки образом, возможность предоставления ответа клиенту после проверки сообщения команды, отправленного в асинхронную очередь, повышает сложность системы по сравнению с внутрипроцессной обработкой команды, при которой результат операции возвращается после выполнения транзакции.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. При использовании очередей может требоваться возвращать результат обработки команды посредством других сообщений о результате операции, для чего в системе необходимы дополнительные компоненты и пользовательские сообщения.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.

Кроме того, асинхронные команды являются односторонними, что во многих случаях не требуется. Об этом говорят Алексей Бурцев (Burtsev Alexey) и Грег Янг (Greg Young) в этой интересной беседе в Интернете: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] 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). Зачем это излишнее усложнение?Why do they introduce this unnecessary complexity? В действительности мне еще не встречался пример кода CQRS с блокирующими обработчиками команд, хотя в большинстве случаев они вполне применимы.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] [...] an asynchronous command doesn't exist; it's actually another event. Если я должен принять то, что вы мне отправили, и породить событие в случае несогласия, значит, вы уже не указываете, что мне нужно что-то сделать [то есть, это не команда].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]. Вы сообщаете мне, что что-то было сделано.It's you telling me something has been done. Сначала разница кажется небольшой, но она имеет множество последствий.This seems like a slight difference at first, but it has many implications.

Асинхронные команды значительно повышают сложность системы, так как нет простого способа сообщать о сбоях.Asynchronous commands greatly increase the complexity of a system, because there is no simple way to indicate failures. Поэтому асинхронные команды рекомендуются только при высоких требованиях к масштабируемости или в особых случаях, когда взаимодействие с внутренними микрослужбами осуществляется посредством сообщений.Therefore, asynchronous commands are not recommended other than when scaling requirements are needed or in special cases when communicating the internal microservices through messaging. В этих ситуациях необходимо спроектировать отдельную систему для информирования о сбоях и восстановления.In those cases, you must design a separate reporting and recovery system for failures.

В первоначальной версии eShopOnContainers было решено использовать синхронную обработку команд, начиная с HTTP-запросов, на основе шаблона медиатора.In the initial version of eShopOnContainers, we decided to use synchronous command processing, started from HTTP requests and driven by the Mediator pattern. Это позволяет легко возвращать результат процесса (успех или неудача), как в реализации CreateOrderCommandHandler.That easily allows you to return the success or failure of the process, as in the CreateOrderCommandHandler implementation.

В любом случае решение должно приниматься на основе бизнес-требований к приложению или микрослужбе.In any case, this should be a decision based on your application’s or microservice’s business requirements.

Реализация конвейера обработки команд с помощью шаблона медиатора (MediatR)Implement the command process pipeline with a mediator pattern (MediatR)

В качестве примера реализации в этом руководстве используется внутрипроцессный конвейер на основе шаблона медиатора, обеспечивающий прием команд и их маршрутизацию в памяти в соответствующие обработчики команд.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. В руководстве также предлагается применять расширения функциональности для разделения сквозной функциональности.The guide also proposes applying behaviors in order to separate cross-cutting concerns.

Для реализации в .NET Core доступно множество библиотек с открытым кодом, которые реализуют шаблон медиатора.For implementation in .NET Core, there are multiple open-source libraries available that implement the Mediator pattern. В этом руководстве применяется библиотека MediatR с открытым кодом (созданная Джимми Богардом (Jimmy Bogard)), но можно выбрать и другой подход.The library used in this guide is the MediatR open-source library (created by Jimmy Bogard), but you could use another approach. MediatR — это небольшая простая библиотека, которая позволяет обрабатывать сообщения в памяти как команды, применяя декораторы или расширения функциональности.MediatR is a small and simple library that allows you to process in-memory messages like a command, while applying decorators or behaviors.

Использование шаблона медиатора помогает уменьшить количество связей и изолировать функциональность, связанную с запрашиваемой работой. При этом вы можете автоматически подключаться к обработчику, который выполняет эту работу — в данном случае к обработчику команд.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.

Еще одна причина для использования шаблона медиатора была раскрыта Джимми Богардом при рецензировании этого руководства:Another good reason to use the Mediator pattern was explained by Jimmy Bogard when reviewing this guide:

"Я думаю, здесь стоит упомянуть тестирование — вы получаете ясное и согласованное представление о поведении системы".I think it might be worth mentioning testing here – it provides a nice consistent window into the behavior of your system. Запрос поступает, ответ выдается — этот принцип оказался очень полезным при разработке согласованно работающих тестов.Request-in, response-out. We’ve found that aspect quite valuable in building consistently behaving tests.

Сначала рассмотрим пример контроллера WebAPI, в котором будет использоваться объект медиатора.First, let’s look at a sample WebAPI controller where you actually would use the mediator object. Если бы объект медиатора не применялся, потребовалось бы внедрить все зависимости для контроллера, такие как средство ведения журнала и другие.If you were not using the mediator object, you would need to inject all the dependencies for that controller, things like a logger object and others. Поэтому конструктор был бы весьма сложным.Therefore, the constructor would be quite complicated. Если же вы используете объект медиатора, конструктор контроллера может быть гораздо проще. Он может иметь всего лишь несколько зависимостей вместо множества, как в случае, когда для каждой сквозной операции имеется отдельная зависимость. Это показано в следующем примере: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)
    // ...

Как можно видеть, медиатор обеспечивает простой контроллер веб-интерфейса API, в котором нет ничего лишнего.You can see that the mediator provides a clean and lean Web API controller constructor. Кроме того, в методах контроллера код для отправки команды объекту медиатора ограничивается практически одной строкой: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();
}

Реализация идемпотентных командImplement idempotent Commands

Более сложным примером по сравнению с приведенным выше в приложении eShopOnContainers является отправка объекта CreateOrderCommand из микрослужбы размещения заказов.In eShopOnContainers, a more advanced example than the above is submitting a CreateOrderCommand object from the Ordering microservice. Однако поскольку бизнес-процесс размещения заказов является немного более сложным и в данном случае он фактически начинается в микрослужбе Basket, действие отправки объекта CreateOrderCommand выполняется из обработчика событий интеграции с именем >UserCheckoutAcceptedIntegrationEvent.cs](https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs), а не из простого контроллера WebAPI, вызываемого из клиентского приложения, как в предыдущем более простом примере.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 >UserCheckoutAcceptedIntegrationEvent.cs](https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs) instead of a simple WebAPI controller called from the client App as in the previous simpler example.

Тем не менее действие отправки команды в MediatR по многом схоже, как показано в приведенном ниже коде.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);

Этот случай также немного сложнее, так как мы реализуем идемпотентные команды.However, this case is also a little bit more advanced because we’re also implementing idempotent commands. Процесс CreateOrderCommand должен быть идемпотентным, чтобы в случае повторной передачи по сети одного и того же сообщения по какой-либо причине, например из-за выполнения повторных попыток, заказ обрабатывался бы только один раз.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.

Для этого бизнес-команда (в данном случае CreateOrderCommand) упаковывается и внедряется в универсальный класс IdentifiedCommand, отслеживаемый по идентификатору каждого поступающего по сети сообщения, которое должно быть идемпотентным.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.

В приведенном ниже коде видно, что IdentifiedCommand — это просто объект DTO с идентификатором и объектом упакованной бизнес-команды.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;
    }
}

Затем обработчик CommandHandler для IdentifiedCommand с именем IdentifiedCommandHandler.cs проверяет, имеется ли уже идентификатор, полученный в сообщении, в таблице.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. Если он имеется, команда не будет обработана повторно, то есть она является идемпотентной.If it already exists, that command won’t be processed again, so it behaves as an idempotent command. Этот код инфраструктуры выполняется в результате вызова метода _requestManager.ExistAsync, приведенного ниже.That infrastructure code is performed by the _requestManager.ExistAsync method call below.

// IdentifiedCommandHandler.cs
public class IdentifiedCommandHandler<T, R> :
                                   IAsyncRequestHandler<IdentifiedCommand<T, R>, R>
                                   where T : IRequest<R>
{
    private readonly IMediator _mediator;
    private readonly IRequestManager _requestManager;

    public IdentifiedCommandHandler(IMediator mediator,
                                    IRequestManager requestManager)
    {
        _mediator = mediator;
        _requestManager = requestManager;
    }

    protected virtual R CreateResultForDuplicateRequest()
    {
        return default(R);
    }

    public async Task<R> Handle(IdentifiedCommand<T, R> message)
    {
        var alreadyExists = await _requestManager.ExistAsync(message.Id);
        if (alreadyExists)
        {
            return CreateResultForDuplicateRequest();
        }
        else
        {
            await _requestManager.CreateRequestForCommandAsync<T>(message.Id);

            // Send the embedded business command to mediator
            // so it runs its related CommandHandler
            var result = await _mediator.Send(message.Command);

            return result;
        }
    }
}

Объект IdentifiedCommand выступает в роли конверта для бизнес-команды, поэтому когда бизнес-команда должна быть обработана в случае, если идентификатор не повторяется, этот объект берет внутреннюю бизнес-команду и повторно передает ее в медиатор. Это продемонстрировано в последней части приведенного выше кода, в которой выполняется метод _mediator.Send(message.Command) из 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 re-submits it to Mediator, as in the last part of the code shown above when running _mediator.Send(message.Command), from the IdentifiedCommandHandler.cs.

При этом выполняется связывание с обработчиком бизнес-команд и его запуск. В данном случае это обработчик CreateOrderCommandHandler, который выполняет транзакции в базе данных Ordering, как показано в приведенном ниже коде.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
                                   : IAsyncRequestHandler<CreateOrderCommand, bool>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IIdentityService _identityService;
    private readonly IMediator _mediator;

    // Using DI to inject infrastructure persistence Repositories
    public CreateOrderCommandHandler(IMediator mediator,
                                     IOrderRepository orderRepository,
                                     IIdentityService identityService)
    {
        _orderRepository = orderRepository ??
                          throw new ArgumentNullException(nameof(orderRepository));
        _identityService = identityService ??
                          throw new ArgumentNullException(nameof(identityService));
        _mediator = mediator ??
                                 throw new ArgumentNullException(nameof(mediator));
    }

    public async Task<bool> Handle(CreateOrderCommand message)
    {
        // Add/Update the Buyer AggregateRoot
        var address = new Address(message.Street, message.City, message.State,
                                  message.Country, message.ZipCode);
        var order = new Order(message.UserId, 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);
        }

        _orderRepository.Add(order);

        return await _orderRepository.UnitOfWork
            .SaveEntitiesAsync();
    }
}

Регистрация типов, используемых библиотекой MediatRRegister the types used by MediatR

Чтобы сообщить библиотеке MediatR об используемых классах обработчиков команд, необходимо зарегистрировать классы медиаторов и обработчиков команд в контейнере 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. По умолчанию библиотека MediatR использует Autofac в качестве контейнера IoC, но вы также можете использовать встроенный контейнер IoC в ASP.NET Core или любой другой контейнер, поддерживаемый 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.

В приведенном ниже коде показано, как зарегистрировать типы и команды Mediator при использовании модулей 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 IAsyncRequestHandler)
        // in assembly holding the Commands
        builder.RegisterAssemblyTypes(
                              typeof(CreateOrderCommand).GetTypeInfo().Assembly).
                                   AsClosedTypesOf(typeof(IAsyncRequestHandler<,>));
        // Other types registration
        //...
    }
}

Именно в нем творится вся магия MediatR.This is where “the magic happens” with MediatR.

Так как каждый обработчик команд реализует универсальный интерфейс IAsyncRequestHandler<T>, при регистрации сборок код регистрирует в RegisteredAssemblyTypes все типы, помеченные как IAsyncRequestHandler. При этом он связывает обработчики CommandHandlers с Commands посредством отношения, определенного в классе CommandHandler, как показано в следующем примере:Because each command handler implements the generic IAsyncRequestHandler<T> interface, when registering the assemblies, the code registers with RegisteredAssemblyTypes all the types marked as IAsyncRequestHandler while relating the CommandHandlers with their Commands, thanks to the relationship stated at the CommandHandler class, as in the following example:

public class CreateOrderCommandHandler
  : IAsyncRequestHandler<CreateOrderCommand, bool>
{

Этот код сопоставляет команды с обработчиками команд.That is the code that correlates commands with command handlers. Обработчик представляет собой простой класс, но он наследуется от RequestHandler<T>, где T обозначает тип команды, и библиотека MediatR обеспечивает его вызов с надлежащими полезными данными (командой).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).

Применение сквозной функциональности при обработке команд с помощью расширений функциональности в MediatRApply cross-cutting concerns when processing commands with the Behaviors in MediatR

Есть еще одна возможность: применение сквозной функциональности к конвейеру медиатора.There is one more thing: being able to apply cross-cutting concerns to the mediator pipeline. В конце кода модуля регистрации Autofac можно заметить, что он регистрирует тип расширения функциональности, а именно: пользовательский класс LoggingBehavior и класс 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. Вы также можете добавить другие пользовательские расширения функциональности.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 IAsyncRequestHandler)
        // in assembly holding the Commands
        builder.RegisterAssemblyTypes(
                              typeof(CreateOrderCommand).GetTypeInfo().Assembly).
                                   AsClosedTypesOf(typeof(IAsyncRequestHandler<,>));
        // Other types registration
        //...
        builder.RegisterGeneric(typeof(LoggingBehavior<,>)).
                                                   As(typeof(IPipelineBehavior<,>));
        builder.RegisterGeneric(typeof(ValidatorBehavior<,>)).
                                                   As(typeof(IPipelineBehavior<,>));
    }
}

Класс LoggingBehavior может быть реализован в виде приведенного ниже кода, который записывает в журнал сведения о выполняемом обработчике команд и результате его выполнения (успех или неудача).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;
    }
}

Если реализовать этот класс расширения функциональности и зарегистрировать его в конвейере (в MediatorModule в примере выше), в журнал будут записываться сведения о выполнении всех команд, обрабатываемых посредством MediatR.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.

В микрослужбе размещения заказов eShopOnContainers применяется еще одно расширение функциональности для проведения базовых проверок. Это класс ValidatorBehavior, основанный на библиотеке FluentValidation и показанный в следующем коде: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;
    }
}

Здесь расширение функциональности вызывает исключение при сбое проверки, но вы также можете возвращать результирующий объект, содержащий результат команды в случае успеха или сообщение проверки в случае сбоя.The behavior here 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. Возможно, это упростит отображение результатов проверки для пользователя.This would probably make it easier to display validation results to the user.

Затем на основе FluentValidation была реализована проверка данных, передаваемых с помощью команды CreateOrderCommand, как показано в следующем коде:Then, based on the FluentValidation library, we created 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();
    }
}

Можно создать дополнительные проверки.You could create additional validations. Это очень простой и изящный способ реализации проверок команд.This is a very clean and elegant way to implement your command validations.

Аналогичным образом можно реализовать дополнительные расширения функциональности для других аспектов или сквозной функциональности, которые требуется применять к командам во время их обработки.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.

Дополнительные ресурсыAdditional resources

Шаблон медиатораThe mediator pattern
Шаблон декоратораThe decorator pattern
MediatR (Джимми Богард (Jimmy Bogard))MediatR (Jimmy Bogard)
Плавная проверкаFluent validation