Sdílet prostřednictvím


Implementace aplikační vrstvy mikroslužeb pomocí webového rozhraní API

Tip

Tento obsah je výňatek z eBooku, architektury mikroslužeb .NET pro kontejnerizované aplikace .NET, které jsou k dispozici na .NET Docs nebo jako zdarma ke stažení PDF, které lze číst offline.

Architektura mikroslužeb .NET pro kontejnerizované eBooky aplikací .NET

Vložení objektů infrastruktury do aplikační vrstvy pomocí injektáže závislostí

Jak už jsme zmínili dříve, aplikační vrstva se dá implementovat jako součást artefaktu (sestavení), které vytváříte, například v rámci projektu webového rozhraní API nebo projektu webové aplikace MVC. V případě mikroslužby vytvořené pomocí ASP.NET Core bude aplikační vrstva obvykle vaší knihovnou webového rozhraní API. Pokud chcete oddělit to, co pochází z ASP.NET Core (jeho infrastruktury a kontrolery) od vlastního kódu aplikační vrstvy, můžete také umístit aplikační vrstvu do samostatné knihovny tříd, ale to je volitelné.

Například kód aplikační vrstvy mikroslužby řazení mikroslužby se přímo implementuje jako součást projektu Ordering.API (projekt webového rozhraní API ASP.NET Core), jak je znázorněno na obrázku 7–23.

Snímek obrazovky mikroslužby Ordering.API v Průzkumník řešení

Průzkumník řešení Zobrazení mikroslužby Ordering.API zobrazující podsložky ve složce Aplikace: Chování, Příkazy, DomainEventHandlers, IntegrationEvents, Models, Dotazy a Ověření.

Obrázek 7–23 Aplikační vrstva v projektu Ordering.API ASP.NET Core Web API

ASP.NET Core zahrnuje jednoduchý integrovaný kontejner IoC (reprezentovaný rozhraním IServiceProvider), který ve výchozím nastavení podporuje injektáž konstruktoru, a ASP.NET zpřístupní určité služby prostřednictvím DI. ASP.NET Core používá termínovou službu pro kterýkoli z typů, které zaregistrujete, které se vloží prostřednictvím DI. V souboru Program.cs vaší aplikace nakonfigurujete integrované služby kontejneru. Vaše závislosti se implementují ve službách, které typ potřebuje, a že se zaregistrujete v kontejneru IoC.

Obvykle chcete vložit závislosti, které implementují objekty infrastruktury. Typická závislost, která se má vložit, je úložiště. Můžete ale vložit jakoukoli jinou závislost infrastruktury, kterou máte. V případě jednodušších implementací můžete přímo vložit objekt modelu práce (EF DbContext), protože DBContext je také implementace objektů trvalosti infrastruktury.

V následujícím příkladu vidíte, jak .NET vkládá požadované objekty úložiště prostřednictvím konstruktoru. Třída je obslužná rutina příkazů, která se probírá v další části.

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

Třída používá vložené úložiště ke spuštění transakce a trvalé změny stavu. Nezáleží na tom, jestli je tato třída obslužnou rutinou příkazů, metodou kontroleru webového rozhraní API ASP.NET Core nebo službou aplikace DDD. Je to nakonec jednoduchá třída, která používá úložiště, entity domény a další koordinaci aplikací způsobem podobným obslužné rutině příkazů. Injektáž závislostí funguje stejně jako u všech zmíněných tříd, jako v příkladu pomocí DI na základě konstruktoru.

Registrace typů implementace závislostí a rozhraní nebo abstrakcí

Než použijete objekty vložené prostřednictvím konstruktorů, potřebujete vědět, kde zaregistrovat rozhraní a třídy, které vytvářejí objekty vložené do tříd aplikace prostřednictvím DI. (Podobně jako DI na základě konstruktoru, jak je znázorněno výše.)

Použití integrovaného kontejneru IoC poskytovaného službou ASP.NET Core

Při použití integrovaného kontejneru IoC poskytovaného službou ASP.NET Core zaregistrujete typy, které chcete vložit do souboru Program.cs , jako v následujícím kódu:

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

Nejběžnějším vzorem při registraci typů v kontejneru IoC je registrace dvojice typů – rozhraní a související třída implementace. Když pak vyžádáte objekt z kontejneru IoC prostřednictvím libovolného konstruktoru, vyžádáte si objekt určitého typu rozhraní. Například v předchozím příkladu poslední řádek uvádí, že pokud některý z vašich konstruktorů má závislost na IMyCustomRepository (rozhraní nebo abstrakce), kontejner IoC vloží instanci MyCustomSQLServerRepository třídy implementace.

Použití knihovny Scrutor pro automatickou registraci typů

Při použití DI v rozhraní .NET můžete být schopni skenovat sestavení a automaticky registrovat jeho typy podle konvence. Tato funkce není aktuálně dostupná v ASP.NET Core. K tomu ale můžete použít knihovnu Scrutor . Tento přístup je vhodný, když máte desítky typů, které je potřeba zaregistrovat v kontejneru IoC.

Další materiály

Použití automatickéhofacu jako kontejneru IoC

Můžete také použít další kontejnery IoC a připojit je do kanálu ASP.NET Core, stejně jako v objednávce mikroslužby v eShopOnContainers, která používá Autofac. Při použití autofacu obvykle zaregistrujete typy prostřednictvím modulů, které umožňují rozdělit typy registrace mezi více souborů v závislosti na tom, kde jsou vaše typy, stejně jako můžete mít typy aplikací distribuované napříč více knihovnami tříd.

Například následující modul aplikace Autofac pro projekt webového rozhraní API Ordering.API s typy, které chcete vložit.

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 má také funkci pro kontrolu sestavení a registrace typů podle konvencí názvů.

Proces registrace a koncepty jsou velmi podobné způsobu, jakým můžete registrovat typy pomocí integrovaného kontejneru IoC ASP.NET Core, ale syntaxe při použití autofacu se trochu liší.

V ukázkovém kódu je abstrakce IOrderRepository registrována spolu s implementační třídou OrderRepository. To znamená, že pokaždé, když konstruktor deklaruje závislost prostřednictvím abstrakce nebo rozhraní IOrderRepository, kontejner IoC vloží instanci OrderRepository třídy.

Typ oboru instance určuje, jak se instance sdílí mezi požadavky na stejnou službu nebo závislost. Když se vytvoří požadavek na závislost, kontejner IoC může vrátit následující:

  • Rozsah jedné instance na dobu životnosti (označovaný v kontejneru ASP.NET Core IoC jako vymezený).

  • Nová instance na závislost (označovaná v kontejneru ASP.NET Core IoC jako přechodná).

  • Jedna instance sdílená napříč všemi objekty pomocí kontejneru IoC (označovaného v kontejneru ASP.NET Core IoC jako singleton).

Další materiály

Implementace vzorů obslužné rutiny příkazů a příkazů

V příkladu di-through-konstruktoru, který je znázorněn v předchozí části, kontejner IoC vkračoval úložiště prostřednictvím konstruktoru ve třídě. Ale přesně kde byly vloženy? V jednoduchém webovém rozhraní API (například mikroslužba katalogu v eShopOnContainers) je vložíte na úrovni kontrolerů MVC v konstruktoru kontroleru jako součást kanálu požadavku ASP.NET Core. V počátečním kódu této části ( CreateOrderCommandHandler třídy ze služby Ordering.API v eShopOnContainers) se injektáž závislostí provádí prostřednictvím konstruktoru konkrétní obslužné rutiny příkazu. Pojďme vysvětlit, co je obslužná rutina příkazu a proč byste ji chtěli použít.

Model Command je vnitřně spojený se vzorem CQRS, který byl zaveden dříve v této příručce. CQRS má dvě strany. První oblastí jsou dotazy pomocí zjednodušených dotazů s mikro ORM Dapperu , které jsme si vysvětlili dříve. Druhou oblastí jsou příkazy, které jsou výchozím bodem transakcí a vstupním kanálem mimo službu.

Jak je znázorněno na obrázku 7–24, model vychází z přijímání příkazů na straně klienta, jejich zpracování na základě pravidel doménového modelu a nakonec zachování stavů s transakcemi.

Diagram znázorňující tok dat vysoké úrovně z klienta do databáze

Obrázek 7–24 Zobrazení příkazů na vysoké úrovni nebo "transakční strany" v modelu CQRS

Obrázek 7–24 ukazuje, že aplikace uživatelského rozhraní odešle příkaz prostřednictvím rozhraní API, které se dostane do CommandHandlerrozhraní API, které závisí na modelu domény a infrastruktuře, k aktualizaci databáze.

Třída příkazu

Příkaz je požadavek, aby systém provedl akci, která změní stav systému. Příkazy jsou imperativní a měly by se zpracovat jen jednou.

Vzhledem k tomu, že příkazy jsou imperativní, obvykle se nazývají slovesem v imperativní náladě (například "create" nebo "update") a můžou obsahovat agregační typ, například CreateOrderCommand. Na rozdíl od události není příkaz faktem z minulosti; je to pouze žádost, a proto může být odmítnuta.

Příkazy můžou pocházet z uživatelského rozhraní v důsledku toho, že uživatel iniciuje požadavek, nebo z správce procesů, když správce procesů nasměruje agregaci na provedení akce.

Důležitou charakteristikou příkazu je, že by měl být zpracován pouze jednou příjemcem. Důvodem je, že příkaz je jedna akce nebo transakce, kterou chcete provést v aplikaci. Například stejný příkaz pro vytvoření objednávky by neměl být zpracován více než jednou. Jedná se o důležitý rozdíl mezi příkazy a událostmi. Události se můžou zpracovávat vícekrát, protože událost může zajímat mnoho systémů nebo mikroslužeb.

Kromě toho je důležité, aby příkaz byl zpracován pouze jednou v případě, že příkaz není idempotentní. Příkaz je idempotentní, pokud se dá spustit několikrát beze změny výsledku, a to buď z důvodu povahy příkazu, nebo kvůli způsobu, jakým systém tento příkaz zpracovává.

Je vhodné, aby příkazy a aktualizace idempotentní, když dává smysl v obchodních pravidlech a invariantech vaší domény. Pokud například z nějakého důvodu (logika opakování, hacking atd.) stejný příkaz CreateOrder dosáhne systému vícekrát, měli byste být schopni ho identifikovat a ujistit se, že nevytváříte více objednávek. Pokud to chcete udělat, musíte v operacích připojit nějaký druh identity a zjistit, jestli už byl příkaz nebo aktualizace zpracována.

Odešlete příkaz jednomu příjemci; příkaz nepublikujete. Publikování je pro události, které uvádějí fakta – něco se stalo a může být zajímavé pro příjemce událostí. V případě událostí nemá vydavatel žádné obavy o to, kteří příjemci událost dostanou nebo co dělají. Události domény nebo integrace jsou ale jiným příběhem, který už byl představen v předchozích částech.

Příkaz je implementován s třídou, která obsahuje datová pole nebo kolekce se všemi informacemi potřebnými k provedení příkazu. Příkaz je speciální druh objektu pro přenos dat (DTO), který se konkrétně používá k vyžádání změn nebo transakcí. Samotný příkaz je založený na přesně informacích, které jsou potřeba ke zpracování příkazu, a nic dalšího.

Následující příklad ukazuje zjednodušenou CreateOrderCommand třídu. Jedná se o neměnný příkaz, který se používá v objednávce mikroslužby v 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; }
    }
}

Třída příkazů v podstatě obsahuje všechna data, která potřebujete k provedení obchodní transakce pomocí objektů doménového modelu. Příkazy jsou tedy jednoduše datové struktury, které obsahují data jen pro čtení a žádné chování. Název příkazu označuje jeho účel. V mnoha jazycích, jako je C#, jsou příkazy reprezentovány jako třídy, ale nejsou to skutečné třídy v reálném objektově orientovaném smyslu.

Příkazy jsou jako další charakteristika neměnné, protože očekávané využití spočívá v tom, že jsou zpracovávány přímo modelem domény. Během předpokládané životnosti se nemusí měnit. Ve třídě jazyka C# lze neměnnosti dosáhnout tím, že nemáte žádné settery ani jiné metody, které mění vnitřní stav.

Mějte na paměti, že pokud máte v úmyslu nebo očekáváte, že příkazy procházejí procesem serializace/deserializace, vlastnosti musí mít privátní setter a [DataMember] (nebo [JsonProperty]) atribut. V opačném případě nebude deserializátor schopen rekonstruovat objekt v cíli s požadovanými hodnotami. Můžete také použít skutečně jen pro čtení vlastnosti, pokud třída má konstruktor s parametry pro všechny vlastnosti, s obvyklým camelCase pojmenování konvence, anotovat konstruktor jako [JsonConstructor]. Tato možnost ale vyžaduje více kódu.

Například třída příkazů pro vytvoření pořadí je pravděpodobně podobná z hlediska dat v pořadí, které chcete vytvořit, ale pravděpodobně nepotřebujete stejné atributy. Například nemá ID objednávky, CreateOrderCommand protože objednávka ještě nebyla vytvořena.

Mnoho tříd příkazů může být jednoduché a vyžaduje pouze několik polí o určitém stavu, které je potřeba změnit. To by bylo v případě, že právě měníte stav objednávky z "v procesu" na "placený" nebo "expedovaný" pomocí příkazu podobného následujícímu:

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

Někteří vývojáři oddělují objekty žádostí o uživatelské rozhraní odděleně od objektů DTO příkazů, ale to je jen věc preference. Jedná se o zdlouhavé oddělení s málo další hodnotou a objekty jsou téměř úplně stejný tvar. Například v eShopOnContainers některé příkazy pocházejí přímo z straně klienta.

Třída obslužné rutiny příkazu

Pro každý příkaz byste měli implementovat konkrétní třídu obslužné rutiny příkazů. To je způsob, jakým model funguje a kde budete používat objekt příkazu, objekty domény a objekty úložiště infrastruktury. Obslužná rutina příkazu je ve skutečnosti jádrem aplikační vrstvy z hlediska CQRS a DDD. Veškerá logika domény by však měla být obsažena v třídách domény – v rámci agregovaných kořenových entit (kořenových entit), podřízených entit nebo doménových služeb, ale ne v obslužné rutině příkazů, což je třída z aplikační vrstvy.

Třída obslužné rutiny příkazů nabízí silný krokovací kámen způsobem, jak dosáhnout SRP (Single Responsibility Principle) zmíněné v předchozí části.

Obslužná rutina příkazu obdrží příkaz a získá výsledek z agregace, která se používá. Výsledkem by mělo být úspěšné spuštění příkazu nebo výjimka. V případě výjimky by se stav systému měl beze změny.

Obslužná rutina příkazu obvykle provádí následující kroky:

  • Obdrží objekt příkazu, například objekt DTO (od mediátora nebo jiného objektu infrastruktury).

  • Ověří, že příkaz je platný (pokud není ověřen mediátorem).

  • Vytvoří instanci agregované kořenové instance, která je cílem aktuálního příkazu.

  • Spustí metodu v agregované kořenové instanci a získá požadovaná data z příkazu.

  • Uchovává nový stav agregace do související databáze. Tato poslední operace je skutečná transakce.

Obslužná rutina příkazu se obvykle zabývá jednou agregací řízenou její agregovanou kořenem (kořenovou entitou). Pokud by na příjem jednoho příkazu mělo mít vliv více agregací, můžete k šíření stavů nebo akcí napříč několika agregacemi použít události domény.

Důležité je, že při zpracování příkazu by veškerá doménová logika měla být uvnitř doménového modelu (agregace), plně zapouzdřená a připravená k testování jednotek. Obslužná rutina příkazu funguje jako způsob, jak získat doménový model z databáze a jako poslední krok informovat vrstvu infrastruktury (úložiště), aby při změně modelu zachovala změny. Výhodou tohoto přístupu je, že můžete refaktorovat logiku domény v izolovaném, plně zapouzdřeném, bohatém, behaviorálním doménovém modelu beze změny kódu ve vrstvách aplikace nebo infrastruktury, což jsou úroveň instalatérské úrovně (obslužné rutiny příkazů, webové rozhraní API, úložiště atd.).

Když obslužné rutiny příkazů získají složitou logiku, může to být zápach kódu. Zkontrolujte je a pokud zjistíte logiku domény, refaktorujte kód tak, aby se toto chování domény přesunulo na metody objektů domény (agregovaná kořenová a podřízená entita).

Jako příklad třídy obslužné rutiny příkazu ukazuje následující kód stejnou CreateOrderCommandHandler třídu, kterou jste viděli na začátku této kapitoly. V tomto případě také zvýrazní metodu Popisovač a operace s objekty a agregacemi doménového modelu.

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

Toto jsou další kroky, které by obslužná rutina příkazu měla provést:

  • Pomocí dat příkazu můžete pracovat s metodami a chováním agregovaného kořenového adresáře.

  • Interně v rámci objektů domény vyvolává události domény během provádění transakce, ale to je transparentní z pohledu obslužné rutiny příkazu.

  • Pokud je výsledek operace agregace úspěšný a po dokončení transakce vyvoláte události integrace. (Tyto třídy infrastruktury můžou být vyvolány také třídami infrastruktury, jako jsou úložiště.)

Další materiály

Kanál procesu příkazu: Jak aktivovat obslužnou rutinu příkazu

Další otázkou je vyvolání obslužné rutiny příkazu. Můžete ho ručně volat z každého souvisejícího kontroleru ASP.NET Core. Tento přístup by však byl příliš propojený a není ideální.

Další dvě hlavní možnosti, které jsou doporučenými možnostmi, jsou:

  • Prostřednictvím artefaktu vzoru mediátora v paměti.

  • S asynchronní frontou zpráv mezi kontrolery a obslužnými rutinami.

Použití vzoru Mediátor (v paměti) v kanálu příkazu

Jak je znázorněno na obrázku 7–25, v přístupu CQRS použijete inteligentní mediátor, který se podobá sběrnici v paměti, která je dostatečně chytrá pro přesměrování na správnou obslužnou rutinu příkazů na základě typu přijatého příkazu nebo DTO. Jednoduché černé šipky mezi komponentami představují závislosti mezi objekty (v mnoha případech vložené prostřednictvím DI) s jejich souvisejícími interakcemi.

Diagram znázorňující podrobnější tok dat z klienta do databáze

Obrázek 7–25 Použití vzoru Mediátor v procesu v jediné mikroslužbě CQRS

Výše uvedený diagram znázorňuje přiblížení z obrázku 7–24: kontroler ASP.NET Core odešle příkaz do kanálu příkazů MediatR, takže se dostanou k příslušné obslužné rutině.

Důvodem, proč použití vzoru Mediátor dává smysl, je, že v podnikových aplikacích může být zpracování požadavků složité. Chcete mít možnost přidat otevřený počet průřezových aspektů, jako je protokolování, ověřování, audit a zabezpečení. V těchto případech se můžete spolehnout na kanál mediátora (viz model mediátora) a poskytnout tak prostředky pro toto dodatečné chování nebo průřezové obavy.

Mediátor je objekt, který zapouzdřuje "způsob" tohoto procesu: koordinuje provádění na základě stavu, způsob vyvolání obslužné rutiny příkazu nebo datovou část, kterou obslužné rutině poskytnete. S mediátorem můžete použít průřezové obavy centralizovaným a transparentním způsobem použitím dekorátorů (nebo chování kanálu od MediatR 3). Další informace najdete v vzoru dekorátoru.

Dekorátory a chování se podobají funkci AOP (Aspect Oriented Programming), která se vztahuje pouze na konkrétní kanál procesu spravovaný komponentou mediátora. Aspekty AOP, které implementují průřezové obavy, se použijí na základě aspektů vložených v době kompilace nebo na základě zachycení volání objektu. Oba typické přístupy AOP se někdy říká, že fungují "jako magie", protože není snadné vidět, jak AOP dělá svou práci. Při řešení závažných problémů nebo chyb může být AOP obtížné ladit. Na druhé straně jsou tyto dekorátory a chování explicitní a používají se pouze v kontextu mediátora, takže ladění je mnohem předvídatelnější a snadné.

Například v eShopOnContainers objednávání mikroslužby má implementaci dvou vzorových chování, LogBehavior třídy a ValidatorBehavior třídy. Implementace chování je vysvětlena v další části tím, že ukazuje, jak eShopOnContainers používá chování MediatR.

Použití front zpráv (mimo proc) v kanálu příkazu

Další možností je použít asynchronní zprávy založené na zprostředkovatelů nebo frontách zpráv, jak je znázorněno na obrázku 7–26. Tato možnost by také mohla být kombinována s mediátorem přímo před obslužnou rutinou příkazu.

Diagram znázorňující tok dat pomocí fronty zpráv vysoké dostupnosti

Obrázek 7–26 Použití front zpráv (mimo proces a komunikace mezi procesy) pomocí příkazů CQRS

Kanál příkazu je možné zpracovat také frontou zpráv s vysokou dostupností, která doručí příkazy příslušné obslužné rutině. Použití front zpráv k přijetí příkazů může dále komplikovat kanál vašeho příkazu, protože pravděpodobně budete muset kanál rozdělit na dva procesy propojené prostřednictvím externí fronty zpráv. Přesto by se měla použít, pokud potřebujete zlepšit škálovatelnost a výkon na základě asynchronního zasílání zpráv. Vezměte v úvahu, že v případě obrázku 7–26 kontroler právě odešle zprávu příkazu do fronty a vrátí se. Obslužné rutiny příkazů pak zprávy zpracovávají vlastním tempem. To je velká výhoda front: fronta zpráv může fungovat jako vyrovnávací paměť v případech, kdy je potřeba hyperšafrovatelnost, například pro akcie nebo jakýkoli jiný scénář s velkým objemem dat příchozího přenosu dat.

Vzhledem k asynchronní povaze front zpráv ale musíte zjistit, jak komunikovat s klientskou aplikací o úspěchu nebo selhání procesu příkazu. Obecně platí, že byste nikdy neměli používat příkazy "fire and forget". Každá obchodní aplikace potřebuje vědět, jestli se příkaz úspěšně zpracoval nebo jestli byl alespoň ověřen a přijat.

Schopnost reagovat na klienta po ověření zprávy příkazu odeslané do asynchronní fronty zvyšuje složitost systému v porovnání s procesem příkazu v procesu, který po spuštění transakce vrátí výsledek operace. Pomocí front možná budete muset vrátit výsledek procesu příkazu prostřednictvím jiných zpráv výsledků operací, které budou vyžadovat další komponenty a vlastní komunikaci ve vašem systému.

Asynchronní příkazy jsou navíc jednosměrné příkazy, které v mnoha případech nemusí být potřeba, jak je vysvětleno v následující zajímavé výměně mezi Burtsev Alexey a Greg Young v online konverzaci:

[Burtsev Alexey] Najdu spoustu kódu, kde lidé používají asynchronní zpracování příkazů nebo jednosměrné zasílání zpráv příkazy bez jakéhokoli důvodu k tomu (neprovádí nějakou dlouhou operaci, nespouštějí externí asynchronní kód, ani přes hranice aplikace, aby používali sběrnici zpráv). Proč zavádějí tuto zbytečnou složitost? A ve skutečnosti jsem neviděl příklad kódu CQRS s blokujícími obslužnými rutinami příkazů, i když to bude ve většině případů fungovat jen dobře.

[Greg Young] [...] asynchronní příkaz neexistuje; je to vlastně jiná událost. Pokud musím přijmout to, co mi pošlete, a vyvolat událost, pokud nesouhlasím, už mi neříkáte, že mám něco udělat [to znamená, že to není příkaz]. Říkáš mi, že se něco udělalo. Vypadá to zpočátku jako mírný rozdíl, ale má to mnoho důsledků.

Asynchronní příkazy výrazně zvyšují složitost systému, protože neexistuje žádný jednoduchý způsob, jak indikovat selhání. Asynchronní příkazy se proto nedoporučují jinak než v případě, že jsou potřeba požadavky na škálování nebo ve zvláštních případech při komunikaci interních mikroslužeb prostřednictvím zasílání zpráv. V těchto případech je nutné navrhnout samostatný systém generování sestav a obnovení pro selhání.

V počáteční verzi eShopOnContainers bylo rozhodnuto použít synchronní zpracování příkazů, zahájené z požadavků HTTP a řízeno modelem Mediátor. To snadno umožňuje vrátit úspěch nebo selhání procesu, jako v CreateOrderCommandHandler implementace.

V každém případě by to mělo být rozhodnutí na základě obchodních požadavků vaší aplikace nebo mikroslužeb.

Implementace kanálu příkazového procesu se vzorem mediátora (MediatR)

Tento průvodce jako ukázkovou implementaci navrhuje použití kanálu v procesu založeného na vzoru Mediátor pro řízení příjmu příkazů a směrování příkazů v paměti do správných obslužných rutin příkazů. Tato příručka také navrhuje uplatňování chování , aby bylo možné oddělit průřezové otázky.

Pro implementaci v .NET je k dispozici několik opensourcových knihoven, které implementují model Mediátor. Knihovna používaná v této příručce je opensourcová knihovna MediatR (vytvořená Jimmy Bogardem), ale můžete použít jiný přístup. MediatR je malá a jednoduchá knihovna, která umožňuje zpracovávat zprávy v paměti jako příkaz při použití dekorátorů nebo chování.

Použití vzoru Mediátor pomáhá snížit párování a izolovat obavy požadované práce a současně se automaticky připojovat k obslužné rutině, která tuto práci provádí – v tomto případě k obslužným rutinám příkazů.

Dalším dobrým důvodem použití vzoru Mediátor byl Jimmy Bogard při prohlížení tohoto průvodce:

Myslím, že by to mohlo být za zmínku o testování zde - poskytuje pěkné konzistentní okno chování vašeho systému. Request-in, response-out. Zjistili jsme, že tento aspekt je poměrně cenný při sestavování konzistentně chovajících se testů.

Nejprve se podíváme na ukázkový kontroler WebAPI, kde byste skutečně použili mediátor objektu. Pokud byste nepoužili mediátor objekt, museli byste vložit všechny závislosti pro tento kontroler, například objekt protokolovacího nástroje a další. Proto by konstruktor byl komplikovaný. Na druhé straně, pokud používáte mediátor objektu, může být konstruktor kontroleru mnohem jednodušší, s několika závislostmi místo mnoha závislostí, pokud jste měli jednu operaci křížového řezu, jako v následujícím příkladu:

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

Vidíte, že mediátor poskytuje čistý a štíhlý konstruktor kontroleru webového rozhraní API. Kromě toho v rámci metod kontroleru je kód pro odeslání příkazu do mediátorového objektu téměř jeden řádek:

[Route("new")]
[HttpPost]
public async Task<IActionResult> ExecuteBusinessOperation([FromBody]RunOpCommand
                                                               runOperationCommand)
{
    var commandResult = await _mediator.SendAsync(runOperationCommand);

    return commandResult ? (IActionResult)Ok() : (IActionResult)BadRequest();
}

Implementace idempotentních příkazů

V eShopOnContainers, pokročilejší příklad, než je výše, odesílá CreateOrderCommand objekt z mikroslužby Ordering. Vzhledem k tomu, že obchodní proces řazení je trochu složitější a v našem případě ve skutečnosti začíná v mikroslužbě Košíku, tato akce odeslání objektu CreateOrderCommand se provádí z obslužné rutiny události integrace s názvem UserCheckoutAcceptedIntegrationEventHandler místo jednoduchého kontroleru WebAPI volaného z klientské aplikace jako v předchozím jednodušším příkladu.

Nicméně akce odeslání příkazu do MediatR je poměrně podobná, jak je znázorněno v následujícím kódu.

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

Tento případ je ale také trochu pokročilejší, protože také implementujeme příkazy idempotentní. Proces CreateOrderCommand by měl být idempotentní, takže pokud se stejná zpráva v síti duplikuje z jakéhokoli důvodu, například opakování, bude stejná obchodní objednávka zpracována jen jednou.

To se implementuje zabalením obchodního příkazu (v tomto případě CreateOrderCommand) a vložením do obecného objektu IdentifiedCommand, který je sledován ID každé zprávy přicházející přes síť, která musí být idempotentní.

V následujícím kódu vidíte, že IdentifiedCommand není nic víc než objekt DTO s ID a zabalený objekt obchodního příkazu.

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

Potom obslužná rutina pro IdentifiedCommand pojmenovaná IdentifiedCommandHandler.cs v podstatě zkontroluje, jestli ID přichází jako součást zprávy již v tabulce. Pokud už existuje, tento příkaz se znovu nezpracuje, takže se chová jako idempotentní příkaz. Tento kód infrastruktury provádí následující _requestManager.ExistAsync volání 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);
            }
        }
    }
}

Vzhledem k tomu, že IdentifiedCommand funguje jako obálka obchodního příkazu, když obchodní příkaz musí být zpracován, protože se nejedná o opakované ID, pak vezme tento vnitřní obchodní příkaz a znovu ho odešle mediátorovi, jak je uvedeno v poslední části výše uvedeného kódu při spuštění _mediator.Send(message.Command), z IdentifiedCommandHandler.cs.

Když to uděláte, propoří a spustí obslužnou rutinu obchodního příkazu, v tomto případě CreateOrderCommandHandler, která spouští transakce proti databázi Ordering, jak je znázorněno v následujícím kódu.

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

Registrace typů používaných mediatR

Aby služba MediatR věděla o třídách obslužné rutiny příkazů, musíte zaregistrovat mediátorské třídy a třídy obslužné rutiny příkazů v kontejneru IoC. MediatR ve výchozím nastavení používá jako kontejner IoC autofac, ale můžete také použít integrovaný kontejner ASP.NET Core IoC nebo jakýkoli jiný kontejner podporovaný mediatR.

Následující kód ukazuje, jak zaregistrovat typy a příkazy mediátora při použití modulů 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
        //...
    }
}

Tady se "magie děje" s MediatR.

Protože každá obslužná rutina příkazů implementuje obecné IRequestHandler<T> rozhraní, při registraci sestavení pomocí RegisteredAssemblyTypes metody všechny typy označené jako IRequestHandler se také zaregistrují s jejich Commands. Příklad:

public class CreateOrderCommandHandler
  : IRequestHandler<CreateOrderCommand, bool>
{

To je kód, který koreluje příkazy s obslužnými rutinami příkazů. Obslužná rutina je pouze jednoduchá třída, ale dědí z RequestHandler<T>, kde T je typ příkazu, a MediatR zajistí, že je vyvolána se správnou datovou částí (příkaz).

Použití průřezových problémů při zpracování příkazů pomocí chování v MediatR

Existuje ještě jedna věc: schopnost uplatňovat na mediátora průřezové obavy. Můžete také vidět na konci kódu modulu registrace Autofac, jak registruje typ chování, konkrétně vlastní LoggingBehavior třídy a ValidatorBehavior třídy. Můžete ale také přidat další vlastní chování.

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

Tuto LogBehavior třídy lze implementovat jako následující kód, který zaznamenává informace o obslužné rutině příkazu, která je spuštěna a zda byla úspěšná nebo ne.

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

Stačí, když tuto třídu chování implementujete a zaregistrujete ji v kanálu (v zprostředkovateliModule výše), všechny příkazy zpracovávané prostřednictvím MediatR budou protokolovat informace o spuštění.

Mikroslužba objednávání eShopOnContainers také používá druhé chování pro základní ověřování, ValidatorBehavior třídy, která spoléhá na knihovnu FluentValidation , jak je znázorněno v následujícím kódu:

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

Toto chování vyvolává výjimku, pokud se ověření nezdaří, ale můžete také vrátit výsledný objekt obsahující výsledek příkazu, pokud byl úspěšný nebo ověřovací zprávy v případě, že ne. To by pravděpodobně usnadnilo zobrazení výsledků ověření uživateli.

Pak byste na základě knihovny FluentValidation vytvořili ověření pro data předaná pomocí CreateOrderCommand, jako v následujícím kódu:

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

Můžete vytvořit další ověření. Jedná se o velmi čistý a elegantní způsob implementace ověření příkazů.

Podobným způsobem byste mohli implementovat další chování pro další aspekty nebo průřezové obavy, které chcete použít u příkazů při jejich zpracování.

Další materiály

Vzor mediátoru
Vzor dekorátoru
MediatR (Jimmy Bogard)
Fluent validation