Implementowanie warstwy aplikacji mikrousług przy użyciu internetowego interfejsu API
Napiwek
Ta zawartość jest fragmentem książki eBook, architektury mikrousług platformy .NET dla konteneryzowanych aplikacji platformy .NET dostępnych na platformie .NET Docs lub jako bezpłatnego pliku PDF, który można odczytać w trybie offline.
Wstrzykiwanie zależności służy do wstrzykiwania obiektów infrastruktury do warstwy aplikacji
Jak wspomniano wcześniej, warstwę aplikacji można zaimplementować w ramach tworzonego artefaktu (zestawu), takiego jak projekt internetowego interfejsu API lub projekt aplikacji internetowej MVC. W przypadku mikrousługi utworzonej przy użyciu platformy ASP.NET Core warstwa aplikacji będzie zwykle biblioteką internetowego interfejsu API. Jeśli chcesz oddzielić elementy pochodzące z ASP.NET Core (jej infrastruktury oraz kontrolerów) z niestandardowego kodu warstwy aplikacji, możesz również umieścić warstwę aplikacji w oddzielnej bibliotece klas, ale jest to opcjonalne.
Na przykład kod warstwy aplikacji mikrousługi porządkowania jest bezpośrednio implementowany w ramach projektu Ordering.API (projekt ASP.NET Core Web API), jak pokazano na rysunku 7–23.
Widok Eksplorator rozwiązań mikrousługi Ordering.API przedstawiający podfoldery w folderze Application: Behaviors, Commands, DomainEventHandlers, IntegrationEvents, Models, Queries i Validations.
Rysunek 7–23. Warstwa aplikacji w projekcie Interfejs API Ordering.API ASP.NET Core Web API
ASP.NET Core zawiera prosty wbudowany kontener IoC (reprezentowany przez interfejs IServiceProvider), który domyślnie obsługuje wstrzykiwanie konstruktora, a ASP.NET udostępnia niektóre usługi za pośrednictwem di. ASP.NET Core używa terminu usługa dla dowolnego typu zarejestrowanego typu, który zostanie wstrzyknięty za pośrednictwem di. Usługi wbudowanego kontenera można skonfigurować w pliku Program.cs aplikacji. Zależności są implementowane w usługach, których typ potrzebuje i które są rejestrowane w kontenerze IoC.
Zazwyczaj należy wstrzyknąć zależności implementujące obiekty infrastruktury. Typowa zależność do wstrzykiwania to repozytorium. Można jednak wstrzyknąć dowolną inną zależność infrastruktury. W przypadku prostszych implementacji można bezpośrednio wstrzyknąć obiekt wzorca Unit of Work (obiekt EF DbContext), ponieważ obiekt DBContext jest również implementacją obiektów trwałości infrastruktury.
W poniższym przykładzie można zobaczyć, jak platforma .NET wprowadza wymagane obiekty repozytorium za pomocą konstruktora. Klasa jest procedurą obsługi poleceń, która zostanie omówiona w następnej sekcji.
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);
}
}
Klasa używa wstrzykiwanych repozytoriów do wykonania transakcji i utrwalania zmian stanu. Nie ma znaczenia, czy ta klasa jest procedurą obsługi poleceń, metodą kontrolera internetowego interfejsu API platformy ASP.NET Core, czy usługą aplikacji DDD. Jest to ostatecznie prosta klasa, która używa repozytoriów, jednostek domeny i innej koordynacji aplikacji w sposób podobny do procedury obsługi poleceń. Wstrzykiwanie zależności działa tak samo jak we wszystkich wymienionych klasach, jak w przykładzie przy użyciu di na podstawie konstruktora.
Rejestrowanie typów implementacji zależności i interfejsów lub abstrakcji
Przed użyciem obiektów wstrzykiwanych za pomocą konstruktorów należy wiedzieć, gdzie zarejestrować interfejsy i klasy, które tworzą obiekty wprowadzone do klas aplikacji za pomocą di. (Podobnie jak di na podstawie konstruktora, jak pokazano wcześniej).
Korzystanie z wbudowanego kontenera IoC dostarczonego przez platformę ASP.NET Core
W przypadku korzystania z wbudowanego kontenera IoC dostarczonego przez platformę ASP.NET Core należy zarejestrować typy, które mają zostać wprowadzone w pliku Program.cs , tak jak w poniższym kodzie:
// Register out-of-the-box framework services.
builder.Services.AddDbContext<CatalogContext>(c =>
c.UseSqlServer(Configuration["ConnectionString"]),
ServiceLifetime.Scoped);
builder.Services.AddMvc();
// Register custom application dependencies.
builder.Services.AddScoped<IMyCustomRepository, MyCustomSQLRepository>();
Najczęstszym wzorcem rejestrowania typów w kontenerze IoC jest zarejestrowanie pary typów — interfejsu i powiązanej z nią klasy implementacji. Następnie, gdy zażądasz obiektu z kontenera IoC za pośrednictwem dowolnego konstruktora, zażądasz obiektu określonego typu interfejsu. Na przykład w poprzednim przykładzie ostatni wiersz stwierdza, że gdy którykolwiek z konstruktorów ma zależność od IMyCustomRepository (interfejs lub abstrakcji), kontener IoC wstrzykuje wystąpienie klasy implementacji MyCustomSQLServerRepository.
Używanie biblioteki Scrutor do automatycznej rejestracji typów
W przypadku korzystania z di di na platformie .NET można skanować zestaw i automatycznie rejestrować jego typy według konwencji. Ta funkcja nie jest obecnie dostępna w programie ASP.NET Core. Można jednak użyć biblioteki Scrutor . Takie podejście jest wygodne, gdy masz dziesiątki typów, które muszą być zarejestrowane w kontenerze IoC.
Dodatkowe zasoby
Mateusz King. Rejestrowanie usług za pomocą narzędzia Scrutor
https://www.mking.net/blog/registering-services-with-scrutorKristian Hellang. Scrutor. Repozytorium GitHub.
https://github.com/khellang/Scrutor
Używanie funkcji Autofac jako kontenera IoC
Możesz również użyć dodatkowych kontenerów IoC i podłączyć je do potoku ASP.NET Core, tak jak w przypadku zamawiania mikrousługi w eShopOnContainers, która używa funkcji Autofac. W przypadku korzystania z funkcji Autofac zazwyczaj rejestrujesz typy za pośrednictwem modułów, co umożliwia podzielenie typów rejestracji między wieloma plikami w zależności od tego, gdzie znajdują się typy, podobnie jak typy aplikacji dystrybuowane między wiele bibliotek klas.
Na przykład poniżej przedstawiono moduł aplikacji Autofac dla projektu internetowego interfejsu API Ordering.API z typami, które chcesz wstrzyknąć.
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 ma również funkcję skanowania zestawów i rejestrowania typów według konwencji nazw.
Proces rejestracji i pojęcia są bardzo podobne do sposobu rejestrowania typów za pomocą wbudowanego kontenera ASP.NET Core IoC, ale składnia podczas korzystania z funkcji Autofac jest nieco inna.
W przykładowym kodzie abstrakcja IOrderRepository jest rejestrowana wraz z repozytorium orderrepository klasy implementacji. Oznacza to, że za każdym razem, gdy konstruktor deklaruje zależność za pośrednictwem abstrakcji lub interfejsu IoCRepository, kontener IoC wstrzykuje wystąpienie klasy OrderRepository.
Typ zakresu wystąpienia określa sposób udostępniania wystąpienia między żądaniami dla tej samej usługi lub zależności. Po wysłaniu żądania dla zależności kontener IoC może zwrócić następujące elementy:
Zakres pojedynczego wystąpienia na okres istnienia (określany w kontenerze ASP.NET Core IoC jako zakres).
Nowe wystąpienie na zależność (określane w kontenerze IoC rdzenia ASP.NET jako przejściowe).
Pojedyncze wystąpienie współużytkowane we wszystkich obiektach przy użyciu kontenera IoC (określanego w kontenerze ASP.NET Core IoC jako pojedynczy).
Dodatkowe zasoby
Wprowadzenie do wstrzykiwania zależności w ASP.NET Core
https://learn.microsoft.com/aspnet/core/fundamentals/dependency-injectionAutofac. Oficjalna dokumentacja.
https://docs.autofac.org/en/latest/Porównanie okresów istnienia usługi kontenera ASP.NET Core IoC z zakresami wystąpień kontenera rozwiązania Autofac IoC — Cesar de la Torre.
https://devblogs.microsoft.com/cesardelatorre/comparing-asp-net-core-ioc-service-life-times-and-autofac-ioc-instance-scopes/
Implementowanie wzorców obsługi poleceń i poleceń
W przykładzie di-through-constructor pokazanym w poprzedniej sekcji kontener IoC wstrzykiwał repozytoria za pomocą konstruktora w klasie. Ale dokładnie tam, gdzie zostały wstrzyknięte? W prostym internetowym interfejsie API (na przykład mikrousługi wykazu w eShopOnContainers) wprowadzasz je na poziomie kontrolerów MVC w konstruktorze kontrolera jako część potoku żądania ASP.NET Core. Jednak w początkowym kodzie tej sekcji ( klasa CreateOrderCommandHandler z usługi Ordering.API w eShopOnContainers) iniekcja zależności odbywa się za pomocą konstruktora określonego programu obsługi poleceń. Wyjaśnijmy, czym jest program obsługi poleceń i dlaczego chcesz go użyć.
Wzorzec polecenia jest wewnętrznie związany ze wzorcem CQRS wprowadzonym wcześniej w tym przewodniku. CQRS ma dwie strony. Pierwszy obszar to zapytania korzystające z uproszczonych zapytań z mikrousługą ORM języka Dapper , co zostało wcześniej wyjaśnione. Drugi obszar to polecenia, które są punktem wyjścia dla transakcji i kanałem wejściowym spoza usługi.
Jak pokazano na rysunku 7–24, wzorzec jest oparty na akceptowaniu poleceń po stronie klienta, przetwarzaniu ich na podstawie reguł modelu domeny i utrwalaniu stanów z transakcjami.
Rysunek 7–24. Ogólny widok poleceń lub "transakcyjnej strony" we wzorcu CQRS
Rysunek 7–24 pokazuje, że aplikacja interfejsu użytkownika wysyła polecenie za pośrednictwem interfejsu API, który przechodzi do CommandHandler
elementu , który zależy od modelu domeny i infrastruktury w celu zaktualizowania bazy danych.
Klasa poleceń
Polecenie to żądanie, aby system wykonał akcję, która zmienia stan systemu. Polecenia są imperatywne i powinny być przetwarzane tylko raz.
Ponieważ polecenia są imperatywne, są one zwykle nazywane czasownikiem w nastroju imperatywnego (na przykład "create" lub "update"), i mogą zawierać typ agregacji, taki jak CreateOrderCommand. W przeciwieństwie do zdarzenia polecenie nie jest faktem z przeszłości; jest to tylko żądanie, a tym samym może zostać odrzucone.
Polecenia mogą pochodzić z interfejsu użytkownika w wyniku zainicjowania żądania przez użytkownika lub menedżera procesów, gdy menedżer procesów kieruje agregację w celu wykonania akcji.
Ważną cechą polecenia jest to, że należy go przetworzyć tylko raz przez pojedynczy odbiornik. Jest to spowodowane tym, że polecenie jest pojedynczą akcją lub transakcją, którą chcesz wykonać w aplikacji. Na przykład to samo polecenie tworzenia zamówienia nie powinno być przetwarzane więcej niż raz. Jest to ważna różnica między poleceniami i zdarzeniami. Zdarzenia mogą być przetwarzane wiele razy, ponieważ wiele systemów lub mikrousług może być zainteresowanych zdarzeniem.
Ponadto ważne jest, aby polecenie było przetwarzane tylko raz, jeśli polecenie nie jest idempotentne. Polecenie jest idempotentne, jeśli można go wykonać wiele razy bez zmiany wyniku, albo ze względu na charakter polecenia, lub ze względu na sposób, w jaki system obsługuje polecenie.
Dobrym rozwiązaniem jest uczynienie poleceń i aktualizacji idempotentnych, gdy ma sens w ramach reguł biznesowych i niezmiennych domeny. Aby na przykład użyć tego samego przykładu, jeśli z jakiegokolwiek powodu (logika ponawiania prób, hacking itp.) to samo polecenie CreateOrder dociera do systemu wiele razy, powinno być możliwe zidentyfikowanie go i upewnienie się, że nie utworzysz wielu zamówień. W tym celu należy dołączyć jakąś tożsamość w operacjach i określić, czy polecenie lub aktualizacja została już przetworzona.
Polecenie jest wysyłane do pojedynczego odbiornika; polecenie nie jest publikowane. Publikowanie dotyczy zdarzeń, które stwierdzają fakt — że coś się stało i może być interesujące dla odbiorców zdarzeń. W przypadku zdarzeń wydawca nie ma obaw o to, którzy odbiorcy otrzymują zdarzenie lub co robią. Jednak zdarzenia domeny lub integracji są inną historią, która została już wprowadzona w poprzednich sekcjach.
Polecenie jest implementowane za pomocą klasy zawierającej pola danych lub kolekcje ze wszystkimi informacjami, które są potrzebne do wykonania tego polecenia. Polecenie to specjalny rodzaj obiektu transferu danych (DTO), który jest specjalnie używany do żądania zmian lub transakcji. Samo polecenie jest oparte na dokładnie tych informacjach, które są potrzebne do przetworzenia polecenia i nic więcej.
W poniższym przykładzie przedstawiono uproszczoną CreateOrderCommand
klasę. Jest to niezmienne polecenie, które jest używane w zamawianiu mikrousługi w eShopOnContainers.
// DDD and CQRS patterns comment: Note that it is recommended to implement immutable Commands
// In this case, its immutability is achieved by having all the setters as private
// plus only being able to update the data just once, when creating the object through its constructor.
// References on Immutable Commands:
// http://cqrs.nu/Faq
// https://docs.spine3.org/motivation/immutability.html
// http://blog.gauffin.org/2012/06/griffin-container-introducing-command-support/
// https://learn.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/how-to-implement-a-lightweight-class-with-auto-implemented-properties
[DataContract]
public class CreateOrderCommand
: IRequest<bool>
{
[DataMember]
private readonly List<OrderItemDTO> _orderItems;
[DataMember]
public string UserId { get; private set; }
[DataMember]
public string UserName { get; private set; }
[DataMember]
public string City { get; private set; }
[DataMember]
public string Street { get; private set; }
[DataMember]
public string State { get; private set; }
[DataMember]
public string Country { get; private set; }
[DataMember]
public string ZipCode { get; private set; }
[DataMember]
public string CardNumber { get; private set; }
[DataMember]
public string CardHolderName { get; private set; }
[DataMember]
public DateTime CardExpiration { get; private set; }
[DataMember]
public string CardSecurityNumber { get; private set; }
[DataMember]
public int CardTypeId { get; private set; }
[DataMember]
public IEnumerable<OrderItemDTO> OrderItems => _orderItems;
public CreateOrderCommand()
{
_orderItems = new List<OrderItemDTO>();
}
public CreateOrderCommand(List<BasketItem> basketItems, string userId, string userName, string city, string street, string state, string country, string zipcode,
string cardNumber, string cardHolderName, DateTime cardExpiration,
string cardSecurityNumber, int cardTypeId) : this()
{
_orderItems = basketItems.ToOrderItemsDTO().ToList();
UserId = userId;
UserName = userName;
City = city;
Street = street;
State = state;
Country = country;
ZipCode = zipcode;
CardNumber = cardNumber;
CardHolderName = cardHolderName;
CardExpiration = cardExpiration;
CardSecurityNumber = cardSecurityNumber;
CardTypeId = cardTypeId;
CardExpiration = cardExpiration;
}
public class OrderItemDTO
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public decimal UnitPrice { get; set; }
public decimal Discount { get; set; }
public int Units { get; set; }
public string PictureUrl { get; set; }
}
}
Zasadniczo klasa poleceń zawiera wszystkie dane potrzebne do wykonania transakcji biznesowej przy użyciu obiektów modelu domeny. W związku z tym polecenia to po prostu struktury danych, które zawierają dane tylko do odczytu i nie działają. Nazwa polecenia wskazuje jego przeznaczenie. W wielu językach, takich jak C#, polecenia są reprezentowane jako klasy, ale nie są to prawdziwe klasy w rzeczywistym sensie obiektowym.
W ramach dodatkowej charakterystyki polecenia są niezmienne, ponieważ oczekiwane użycie polega na tym, że są przetwarzane bezpośrednio przez model domeny. Nie muszą zmieniać się w okresie ich przewidywanego okresu istnienia. W klasie C# niezmienność można osiągnąć, nie mając żadnych metod ustawiających ani innych metod, które zmieniają stan wewnętrzny.
Należy pamiętać, że jeśli zamierzasz lub oczekujesz, że polecenia przejdą przez proces serializacji/deserializacji, właściwości muszą mieć zestaw prywatny i [DataMember]
atrybut (lub [JsonProperty]
). W przeciwnym razie deserializator nie będzie mógł odtworzyć obiektu w miejscu docelowym z wymaganymi wartościami. Można również użyć prawdziwie tylko do odczytu właściwości, jeśli klasa ma konstruktor z parametrami dla wszystkich właściwości, ze zwykłą konwencją nazewnictwa camelCase i dodawać adnotacje konstruktora jako [JsonConstructor]
. Jednak ta opcja wymaga więcej kodu.
Na przykład klasa poleceń do tworzenia zamówienia jest prawdopodobnie podobna pod względem danych do kolejności, którą chcesz utworzyć, ale prawdopodobnie nie potrzebujesz tych samych atrybutów. Na przykład nie ma identyfikatora zamówienia, CreateOrderCommand
ponieważ zamówienie nie zostało jeszcze utworzone.
Wiele klas poleceń może być prostych, co wymaga tylko kilku pól dotyczących pewnego stanu, które należy zmienić. Tak byłoby w przypadku zmiany stanu zamówienia z "w procesie" na "płatne" lub "wysłane" przy użyciu polecenia podobnego do następującego:
[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; }
}
Niektórzy deweloperzy tworzą obiekty żądań interfejsu użytkownika oddzielone od obiektów DTO poleceń, ale jest to tylko kwestia preferencji. Jest to żmudne rozdzielenie z nie dużą dodatkową wartością, a obiekty są prawie dokładnie tym samym kształtem. Na przykład w eShopOnContainers niektóre polecenia pochodzą bezpośrednio z klienta.
Klasa obsługi poleceń
Należy zaimplementować określoną klasę obsługi poleceń dla każdego polecenia. W ten sposób działa wzorzec, w którym będzie używany obiekt polecenia, obiekty domeny i obiekty repozytorium infrastruktury. Procedura obsługi poleceń jest w rzeczywistości sercem warstwy aplikacji pod względem CQRS i DDD. Jednak cała logika domeny powinna być zawarta w klasach domeny — w ramach agregacji katalogów głównych (jednostek głównych), jednostek podrzędnych lub usług domenowych, ale nie w programie obsługi poleceń, która jest klasą z warstwy aplikacji.
Klasa obsługi poleceń oferuje silny kamień krokowy w celu osiągnięcia zasady pojedynczej odpowiedzialności (SRP) wymienionej w poprzedniej sekcji.
Procedura obsługi poleceń odbiera polecenie i uzyskuje wynik z agregacji, która jest używana. Wynikiem powinno być pomyślne wykonanie polecenia lub wyjątek. W przypadku wyjątku stan systemu powinien być niezmieniony.
Procedura obsługi poleceń zwykle wykonuje następujące czynności:
Otrzymuje obiekt polecenia, taki jak DTO (od mediatora lub innego obiektu infrastruktury).
Sprawdza, czy polecenie jest prawidłowe (jeśli nie zostało zweryfikowane przez mediatora).
Tworzy wystąpienie zagregowanego wystąpienia głównego, które jest celem bieżącego polecenia.
Wykonuje metodę w zagregowanym wystąpieniu głównym, uzyskując wymagane dane z polecenia .
Utrzymuje on nowy stan agregacji do powiązanej bazy danych. Ta ostatnia operacja jest rzeczywistą transakcją.
Zazwyczaj procedura obsługi poleceń dotyczy pojedynczego agregacji opartego na jego zagregowanym katalogu głównym (jednostce głównej). Jeśli wiele agregacji powinno mieć wpływ na odebranie jednego polecenia, można użyć zdarzeń domeny do propagowania stanów lub akcji w wielu agregacjach.
Ważnym punktem jest to, że gdy polecenie jest przetwarzane, cała logika domeny powinna znajdować się wewnątrz modelu domeny (agregacji), w pełni hermetyzowane i gotowe do testowania jednostkowego. Procedura obsługi poleceń działa tak samo jak sposób na pobranie modelu domeny z bazy danych i jako ostatni krok, aby poinformować warstwę infrastruktury (repozytoria) o utrwalaniu zmian podczas zmiany modelu. Zaletą tego podejścia jest możliwość refaktoryzacji logiki domeny w izolowanym, w pełni hermetyzowanym, bogatym, behawioralnym modelu domeny bez konieczności zmieniania kodu w warstwach aplikacji lub infrastruktury, które są poziomem kanalizacji (programy obsługi poleceń, internetowy interfejs API, repozytoria itp.).
Gdy programy obsługi poleceń są złożone, z zbyt dużą logią, może to być zapach kodu. Przejrzyj je, a jeśli znajdziesz logikę domeny, refaktoryzuj kod, aby przenieść to zachowanie domeny do metod obiektów domeny (agregacji głównej i podrzędnej jednostki).
Jako przykład klasy obsługi poleceń poniższy kod przedstawia tę samą CreateOrderCommandHandler
klasę, która była wyświetlana na początku tego rozdziału. W tym przypadku wyróżnia również metodę Handle i operacje z obiektami/agregacjami modelu domeny.
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);
}
}
Oto dodatkowe kroki, które należy wykonać w programie obsługi poleceń:
Użyj danych polecenia, aby pracować z metodami i zachowaniem zagregowanego katalogu głównego.
Wewnętrznie w obiektach domeny zgłaszaj zdarzenia domeny podczas wykonywania transakcji, ale jest to niewidoczne z punktu widzenia programu obsługi poleceń.
Jeśli wynik operacji agregacji zakończy się pomyślnie i po zakończeniu transakcji, zgłoś zdarzenia integracji. (Mogą one być również wywoływane przez klasy infrastruktury, takie jak repozytoria).
Dodatkowe zasoby
Mark Seemann. Na granicach aplikacje nie są obiektowe
https://blog.ploeh.dk/2011/05/31/AttheBoundaries,ApplicationsareNotObject-Oriented/Polecenia i zdarzenia
https://cqrs.nu/faq/Command%20and%20EventsCo robi procedura obsługi poleceń?
https://cqrs.nu/faq/Command%20HandlersJimmy Bogard. Wzorce poleceń domeny — programy obsługi
https://jimmybogard.com/domain-command-patterns-handlers/Jimmy Bogard. Wzorce poleceń domeny — walidacja
https://jimmybogard.com/domain-command-patterns-validation/
Potok procesu polecenia: jak wyzwolić procedurę obsługi poleceń
Następne pytanie brzmi, jak wywołać procedurę obsługi poleceń. Można go ręcznie wywołać z każdego powiązanego kontrolera ASP.NET Core. Jednak takie podejście byłoby zbyt sprzężone i nie jest idealne.
Pozostałe dwie główne opcje, które są zalecanymi opcjami, to:
Za pomocą artefaktu wzorca mediatora w pamięci.
W przypadku kolejki komunikatów asynchronicznych między kontrolerami i procedurami obsługi.
Użyj wzorca Mediatora (w pamięci) w potoku poleceń
Jak pokazano na rysunku 7–25, w podejściu CQRS należy użyć inteligentnego mediatora, podobnego do magistrali w pamięci, która jest wystarczająco inteligentna, aby przekierować do odpowiedniego programu obsługi poleceń na podstawie typu odbieranego polecenia lub DTO. Pojedyncze czarne strzałki między składnikami reprezentują zależności między obiektami (w wielu przypadkach wstrzykiwane przez DI) z ich powiązanymi interakcjami.
Rysunek 7–25. Używanie wzorca Mediatora w procesie w jednej mikrousłudze CQRS
Na powyższym diagramie przedstawiono powiększenie obrazu 7-24: kontroler ASP.NET Core wysyła polecenie do potoku poleceń MediatR, aby uzyskać odpowiednią procedurę obsługi.
Powodem, dla którego korzystanie ze wzorca Mediatora ma sens, jest to, że w aplikacjach dla przedsiębiorstw żądania przetwarzania mogą być skomplikowane. Chcesz mieć możliwość dodania otwartej liczby zagadnień krzyżowych, takich jak rejestrowanie, walidacje, inspekcja i zabezpieczenia. W takich przypadkach można polegać na potoku mediatora (zob . wzorzec Mediatora), aby zapewnić środki dla tych dodatkowych zachowań lub zagadnień przekrojowych.
Mediator jest obiektem, który hermetyzuje "sposób" tego procesu: koordynuje wykonywanie na podstawie stanu, sposób wywoływanego programu obsługi poleceń lub ładunku podanego dla programu obsługi. Dzięki składnikowi mediatora można zastosować kwestie przekrojowe w scentralizowany i przejrzysty sposób, stosując dekoratory (lub zachowania potoku od MediatR 3). Aby uzyskać więcej informacji, zobacz wzorzec dekoratora.
Dekoratory i zachowania są podobne do programowania zorientowanego na aspekty (AOP), stosowane tylko do określonego potoku procesu zarządzanego przez składnik mediatora. Aspekty w programie AOP, które implementują problemy związane z wycinaniem krzyżowym, są stosowane na podstawie elementów tkaczy aspektowych wstrzykiwanych w czasie kompilacji lub na podstawie przechwytywania wywołań obiektu. Oba typowe podejścia AOP są czasami mówi się, że działają "jak magia", ponieważ nie jest łatwo zobaczyć, jak AOP wykonuje swoją pracę. W przypadku poważnych problemów lub usterek debugowanie AOP może być trudne. Z drugiej strony te dekoratory/zachowania są jawne i stosowane tylko w kontekście mediatora, więc debugowanie jest znacznie bardziej przewidywalne i łatwe.
Na przykład w klasie eShopOnContainers zamawiania mikrousługi implementacja dwóch przykładowych zachowań, klasa LogBehavior i klasa ValidatorBehavior . Implementacja zachowań jest wyjaśniona w następnej sekcji, pokazując, jak eShopOnContainers używa zachowań MediatR.
Używanie kolejek komunikatów (out-of-proc) w potoku polecenia
Innym wyborem jest użycie komunikatów asynchronicznych opartych na brokerach lub kolejkach komunikatów, jak pokazano na rysunku 7–26. Tę opcję można również połączyć ze składnikiem mediatora bezpośrednio przed procedurą obsługi poleceń.
Rysunek 7–26. Używanie kolejek komunikatów (poza procesem i komunikacją między procesami) za pomocą poleceń CQRS
Potok polecenia może być również obsługiwany przez kolejkę komunikatów o wysokiej dostępności, aby dostarczyć polecenia do odpowiedniej procedury obsługi. Użycie kolejek komunikatów do akceptowania poleceń może jeszcze bardziej skomplikować potok polecenia, ponieważ prawdopodobnie trzeba będzie podzielić potok na dwa procesy połączone za pośrednictwem kolejki komunikatów zewnętrznych. Mimo to należy go użyć, jeśli trzeba zwiększyć skalowalność i wydajność na podstawie asynchronicznej obsługi komunikatów. Należy wziąć pod uwagę, że w przypadku rysunku 7-26 kontroler po prostu publikuje komunikat polecenia w kolejce i zwraca. Następnie programy obsługi poleceń przetwarzają komunikaty we własnym tempie. Jest to świetna zaleta kolejek: kolejka komunikatów może działać jako bufor w przypadkach, gdy wymagana jest hiperskalność, na przykład w przypadku zasobów lub dowolnego innego scenariusza z dużą ilością danych przychodzących.
Jednak ze względu na asynchroniczny charakter kolejek komunikatów należy ustalić, jak komunikować się z aplikacją kliencką o powodzeniu lub niepowodzeniu procesu polecenia. Zgodnie z regułą nigdy nie należy używać poleceń "fire and forget". Każda aplikacja biznesowa musi wiedzieć, czy polecenie zostało pomyślnie przetworzone, czy przynajmniej zweryfikowane i zaakceptowane.
W związku z tym możliwość odpowiadania klientowi po zweryfikowaniu komunikatu polecenia przesłanego do kolejki asynchronicznej zwiększa złożoność systemu w porównaniu z procesem polecenia w procesie, który zwraca wynik operacji po uruchomieniu transakcji. Korzystając z kolejek, może być konieczne zwrócenie wyniku procesu polecenia za pośrednictwem innych komunikatów wyników operacji, które będą wymagały dodatkowych składników i niestandardowej komunikacji w systemie.
Ponadto polecenia asynchroniczne to polecenia jednokierunkowe, które w wielu przypadkach mogą nie być potrzebne, jak wyjaśniono w następującej interesującej wymiany między Burtsev Alexey i Greg Young w rozmowie online:
[Burtsev Alexey] Uważam, że wiele kodu, w którym ludzie używają asynchronicznego obsługi poleceń lub jednokierunkowego komunikatów poleceń bez żadnego powodu, aby to zrobić (nie wykonują długiej operacji, nie wykonują zewnętrznego kodu asynchronicznego, nie są nawet granicą między aplikacjami, aby używać magistrali komunikatów). Dlaczego wprowadzają tę niepotrzebną złożoność? I rzeczywiście, nie widziałem przykładu kodu CQRS z blokowaniem procedur obsługi poleceń do tej pory, choć będzie działać dobrze w większości przypadków.
[Greg Young] [...] polecenie asynchroniczne nie istnieje; to w rzeczywistości kolejne wydarzenie. Jeśli muszę zaakceptować to, co wysyłasz do mnie i zgłaszać wydarzenie, jeśli się nie zgadzam, to już nie mówisz mi, abym zrobił coś [to jest, to nie jest polecenie]. Mówisz mi, że coś zostało zrobione. Początkowo wydaje się to niewielką różnicą, ale ma wiele konsekwencji.
Polecenia asynchroniczne znacznie zwiększają złożoność systemu, ponieważ nie ma prostego sposobu wskazywania awarii. W związku z tym polecenia asynchroniczne nie są zalecane poza wymaganiami dotyczącymi skalowania lub w specjalnych przypadkach podczas komunikowania się z wewnętrznymi mikrousługami za pośrednictwem komunikatów. W takich przypadkach należy zaprojektować oddzielny system raportowania i odzyskiwania pod kątem awarii.
W początkowej wersji eShopOnContainers podjęto decyzję o użyciu synchronicznego przetwarzania poleceń, rozpoczętego od żądań HTTP i sterowanych wzorcem mediatora. Umożliwia to łatwe zwrócenie powodzenia lub niepowodzenia procesu, jak w implementacji CreateOrderCommandHandler .
W każdym razie powinna to być decyzja oparta na wymaganiach biznesowych aplikacji lub mikrousługi.
Implementowanie potoku procesu poleceń za pomocą wzorca mediatora (MediatR)
W ramach przykładowej implementacji ten przewodnik proponuje użycie potoku procesu opartego na wzorcu Mediatora w celu napędzania pozyskiwania poleceń i kierowania poleceń w pamięci do odpowiednich procedur obsługi poleceń. W przewodniku zaproponowano również stosowanie zachowań w celu oddzielenia zagadnień związanych z cięciem krzyżowym.
W przypadku implementacji na platformie .NET dostępnych jest wiele bibliotek typu open source, które implementują wzorzec mediatora. Biblioteka używana w tym przewodniku to biblioteka open source MediatR (utworzona przez Jimmy'ego Bogarda), ale można użyć innego podejścia. MediatR to mała i prosta biblioteka, która umożliwia przetwarzanie komunikatów w pamięci, takich jak polecenie, podczas stosowania dekoratorów lub zachowań.
Użycie wzorca Mediatora pomaga zmniejszyć sprzężenie i odizolować obawy żądanej pracy, podczas gdy automatycznie łączy się z procedurą obsługi wykonującą tę pracę — w tym przypadku do obsługi poleceń.
Innym dobrym powodem, aby użyć wzorca Mediatora, zostało wyjaśnione przez Jimmy'ego Bogarda podczas przeglądania tego przewodnika:
Myślę, że warto wspomnieć o testach tutaj — zapewnia ładne spójne okno na zachowanie systemu. Żądanie w odpowiedzi. Odkryliśmy, że aspekt jest bardzo cenny w tworzeniu konsekwentnie zachowywających się testów.
Najpierw przyjrzyjmy się przykładowej kontrolerowi WebAPI, w którym rzeczywiście można użyć obiektu mediatora. Jeśli nie używasz obiektu mediatora, musisz wstrzyknąć wszystkie zależności dla tego kontrolera, takie jak obiekt rejestratora i inne. W związku z tym konstruktor byłby skomplikowany. Z drugiej strony, jeśli używasz obiektu mediatora, konstruktor kontrolera może być znacznie prostszy, z zaledwie kilkoma zależnościami zamiast wielu zależności, jeśli miał jeden na operację wycinania krzyżowego, jak w poniższym przykładzie:
public class MyMicroserviceController : Controller
{
public MyMicroserviceController(IMediator mediator,
IMyMicroserviceQueries microserviceQueries)
{
// ...
}
}
Widać, że mediator zapewnia czysty i chudy konstruktor kontrolera internetowego interfejsu API. Ponadto w ramach metod kontrolera kod wysyłający polecenie do obiektu mediatora jest prawie jednym wierszem:
[Route("new")]
[HttpPost]
public async Task<IActionResult> ExecuteBusinessOperation([FromBody]RunOpCommand
runOperationCommand)
{
var commandResult = await _mediator.SendAsync(runOperationCommand);
return commandResult ? (IActionResult)Ok() : (IActionResult)BadRequest();
}
Implementowanie poleceń idempotentnych
W eShopOnContainers bardziej zaawansowany przykład niż powyżej przesyła obiekt CreateOrderCommand z mikrousługi Ordering. Jednak ponieważ proces biznesowy zamawiania jest nieco bardziej złożony i w naszym przypadku faktycznie rozpoczyna się w mikrousłudze Koszyk, ta akcja przesyłania obiektu CreateOrderCommand jest wykonywana z procedury obsługi zdarzeń integracji o nazwie UserCheckoutAcceptedIntegrationEventHandler zamiast prostego kontrolera WebAPI wywoływanego z aplikacji klienckiej, jak w poprzednim prostszym przykładzie.
Niemniej jednak akcja przesłania polecenia do mediatR jest dość podobna, jak pokazano w poniższym kodzie.
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);
Jednak ten przypadek jest również nieco bardziej zaawansowany, ponieważ implementujemy również polecenia idempotentne. Proces CreateOrderCommand powinien być idempotentny, więc jeśli ten sam komunikat zostanie zduplikowany za pośrednictwem sieci, z jakiegokolwiek powodu, na przykład ponawianie prób, to to samo zamówienie biznesowe zostanie przetworzone tylko raz.
Jest to implementowane przez zawijanie polecenia biznesowego (w tym przypadku CreateOrderCommand) i osadzanie go w ogólnym ZidentyfikowanePolecenia, które jest śledzone przez identyfikator każdego komunikatu przychodzącego przez sieć, która musi być idempotentna.
W poniższym kodzie widać, że wartość IdentifiedCommand jest niczym więcej niż obiektem DTO i identyfikatorem oraz obiektem zawiniętego polecenia biznesowego.
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;
}
}
Następnie program CommandHandler dla zidentyfikowanegopolecenia o nazwie IdentifiedCommandHandler.cs w zasadzie sprawdzi, czy identyfikator przychodzący jako część komunikatu już istnieje w tabeli. Jeśli już istnieje, to polecenie nie zostanie ponownie przetworzone, więc zachowuje się jako idempotentne polecenie. Ten kod infrastruktury jest wykonywany przez _requestManager.ExistAsync
poniższe wywołanie metody.
// IdentifiedCommandHandler.cs
public class IdentifiedCommandHandler<T, R> : IRequestHandler<IdentifiedCommand<T, R>, R>
where T : IRequest<R>
{
private readonly IMediator _mediator;
private readonly IRequestManager _requestManager;
private readonly ILogger<IdentifiedCommandHandler<T, R>> _logger;
public IdentifiedCommandHandler(
IMediator mediator,
IRequestManager requestManager,
ILogger<IdentifiedCommandHandler<T, R>> logger)
{
_mediator = mediator;
_requestManager = requestManager;
_logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
}
/// <summary>
/// Creates the result value to return if a previous request was found
/// </summary>
/// <returns></returns>
protected virtual R CreateResultForDuplicateRequest()
{
return default(R);
}
/// <summary>
/// This method handles the command. It just ensures that no other request exists with the same ID, and if this is the case
/// just enqueues the original inner command.
/// </summary>
/// <param name="message">IdentifiedCommand which contains both original command & request ID</param>
/// <returns>Return value of inner command or default value if request same ID was found</returns>
public async Task<R> Handle(IdentifiedCommand<T, R> message, CancellationToken cancellationToken)
{
var alreadyExists = await _requestManager.ExistAsync(message.Id);
if (alreadyExists)
{
return CreateResultForDuplicateRequest();
}
else
{
await _requestManager.CreateRequestForCommandAsync<T>(message.Id);
try
{
var command = message.Command;
var commandName = command.GetGenericTypeName();
var idProperty = string.Empty;
var commandId = string.Empty;
switch (command)
{
case CreateOrderCommand createOrderCommand:
idProperty = nameof(createOrderCommand.UserId);
commandId = createOrderCommand.UserId;
break;
case CancelOrderCommand cancelOrderCommand:
idProperty = nameof(cancelOrderCommand.OrderNumber);
commandId = $"{cancelOrderCommand.OrderNumber}";
break;
case ShipOrderCommand shipOrderCommand:
idProperty = nameof(shipOrderCommand.OrderNumber);
commandId = $"{shipOrderCommand.OrderNumber}";
break;
default:
idProperty = "Id?";
commandId = "n/a";
break;
}
_logger.LogInformation(
"----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})",
commandName,
idProperty,
commandId,
command);
// Send the embedded business command to mediator so it runs its related CommandHandler
var result = await _mediator.Send(command, cancellationToken);
_logger.LogInformation(
"----- Command result: {@Result} - {CommandName} - {IdProperty}: {CommandId} ({@Command})",
result,
commandName,
idProperty,
commandId,
command);
return result;
}
catch
{
return default(R);
}
}
}
}
Ponieważ ZidentyfikowanePolecenia działa jak koperta polecenia biznesowego, gdy polecenie biznesowe musi zostać przetworzone, ponieważ nie jest to powtarzający się identyfikator, to przyjmuje to wewnętrzne polecenie biznesowe i przesyła je ponownie do Mediatora, jak w ostatniej części kodu pokazanego powyżej podczas uruchamiania _mediator.Send(message.Command)
, z IdentifiedCommandHandler.cs.
W takim przypadku połączy i uruchomi procedurę obsługi poleceń biznesowych, w tym przypadku createOrderCommandHandler, która uruchamia transakcje względem bazy danych Ordering, jak pokazano w poniższym kodzie.
// 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);
}
}
Rejestrowanie typów używanych przez mediatR
Aby usługa MediatR wiedziała o klasach obsługi poleceń, należy zarejestrować klasy mediatora i klasy obsługi poleceń w kontenerze IoC. Domyślnie usługa MediatR używa funkcji Autofac jako kontenera IoC, ale można również użyć wbudowanego kontenera ASP.NET Core IoC lub dowolnego innego kontenera obsługiwanego przez usługę MediatR.
Poniższy kod przedstawia sposób rejestrowania typów i poleceń mediatora podczas korzystania z modułów autofac.
public class MediatorModule : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly)
.AsImplementedInterfaces();
// Register all the Command classes (they implement IRequestHandler)
// in assembly holding the Commands
builder.RegisterAssemblyTypes(typeof(CreateOrderCommand).GetTypeInfo().Assembly)
.AsClosedTypesOf(typeof(IRequestHandler<,>));
// Other types registration
//...
}
}
Jest to miejsce, w którym "magia dzieje się" z MediatR.
Ponieważ każda procedura obsługi poleceń implementuje interfejs ogólnyIRequestHandler<T>
, podczas rejestrowania zestawów przy użyciu metody wszystkie typy oznaczone IRequestHandler
jako również są rejestrowane przy użyciu RegisteredAssemblyTypes
ich Commands
. Na przykład:
public class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, bool>
{
Jest to kod, który koreluje polecenia z procedurami obsługi poleceń. Procedura obsługi jest tylko prostą klasą, ale dziedziczy z RequestHandler<T>
klasy , gdzie T jest typem polecenia, a mediatR upewnia się, że jest wywoływany przy użyciu poprawnego ładunku (polecenie).
Stosowanie zagadnień krzyżowych podczas przetwarzania poleceń za pomocą zachowań w mediatR
Jest jeszcze jedna rzecz: możliwość zastosowania kwestii krzyżowych do rurociągu mediatora. Na końcu kodu modułu rejestracji autofak można również zobaczyć, jak rejestruje typ zachowania, w szczególności niestandardową klasę LoggingBehavior i klasę ValidatorBehavior. Można jednak również dodać inne zachowania niestandardowe.
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<,>));
}
}
Ta klasa LoggingBehavior może być zaimplementowana jako następujący kod, który rejestruje informacje o wykonywanym programie obsługi poleceń i informację o tym, czy zakończyła się pomyślnie, czy nie.
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;
}
}
Po wdrożeniu tej klasy zachowania i zarejestrowaniu jej w potoku (powyżej w trybie MediatorModule) wszystkie polecenia przetwarzane za pośrednictwem mediatR będą rejestrować informacje o wykonaniu.
EShopOnContainers porządkowanie mikrousługi stosuje również drugie zachowanie dla podstawowych weryfikacji, klasę ValidatorBehavior , która opiera się na bibliotece FluentValidation , jak pokazano w poniższym kodzie:
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;
}
}
W tym przypadku zachowanie zgłasza wyjątek w przypadku niepowodzenia walidacji, ale można również zwrócić obiekt wynikowy zawierający wynik polecenia, jeśli zakończył się pomyślnie lub w przypadku, gdy nie został zwrócony. Prawdopodobnie ułatwi to wyświetlenie wyników walidacji użytkownikowi.
Następnie na podstawie biblioteki FluentValidation utworzysz walidację danych przekazanych za pomocą polecenia CreateOrderCommand, jak w poniższym kodzie:
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();
}
}
Można utworzyć dodatkowe walidacje. Jest to bardzo czysty i elegancki sposób implementacji weryfikacji poleceń.
W podobny sposób można zaimplementować inne zachowania dla dodatkowych aspektów lub zagadnień krzyżowych, które mają być stosowane do poleceń podczas ich obsługi.
Dodatkowe zasoby
Wzorzec mediatora
- Wzorzec mediatora
https://en.wikipedia.org/wiki/Mediator_pattern
Deseń dekoratora
- Deseń dekoratora
https://en.wikipedia.org/wiki/Decorator_pattern
MediatR (Jimmy Bogard)
MediatR. Repozytorium GitHub.
https://github.com/jbogard/MediatRCQRS z mediatR i automapper
https://lostechies.com/jimmybogard/2015/05/05/cqrs-with-mediatr-and-automapper/Umieść kontrolery na diecie: POSTs i polecenia.
https://lostechies.com/jimmybogard/2013/12/19/put-your-controllers-on-a-diet-posts-and-commands/Przeciwdziałanie problemom krzyżowym z rurociągiem mediatora
https://lostechies.com/jimmybogard/2014/09/09/tackling-cross-cutting-concerns-with-a-mediator-pipeline/CQRS i REST: idealne dopasowanie
https://lostechies.com/jimmybogard/2016/06/01/cqrs-and-rest-the-perfect-match/Przykłady potoku MediatR
https://lostechies.com/jimmybogard/2016/10/13/mediatr-pipeline-examples/Pionowe urządzenia testowe fragmentatora dla mediatR i ASP.NET Core
https://lostechies.com/jimmybogard/2016/10/24/vertical-slice-test-fixtures-for-mediatr-and-asp-net-core/Wydano rozszerzenia MediatR na potrzeby wstrzykiwania zależności firmy Microsoft
https://lostechies.com/jimmybogard/2016/07/19/mediatr-extensions-for-microsoft-dependency-injection-released/
Płynna walidacja
- Jeremy Skinner. FluentValidation. Repozytorium GitHub.
https://github.com/JeremySkinner/FluentValidation