Megosztás a következőn keresztül:


A mikroszolgáltatási alkalmazásréteg implementálása a Webes API használatával

Tipp.

Ez a tartalom egy részlet a .NET-alkalmazásokhoz készült .NET-alkalmazásokhoz készült eBook, .NET Microservices Architecture című eBookból, amely elérhető a .NET Docs-on vagy egy ingyenesen letölthető PDF-fájlként, amely offline módban is olvasható.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

Infrastruktúra-objektumok injektálása az alkalmazásrétegbe a Függőséginjektálás használatával

Ahogy korábban említettük, az alkalmazásréteg implementálható az ön által létrehozott összetevő (szerelvény) részeként, például egy Web API-projektben vagy egy MVC-webalkalmazás-projektben. A ASP.NET Core használatával létrehozott mikroszolgáltatások esetében az alkalmazásréteg általában a webes API-kódtár lesz. Ha el szeretné különíteni a ASP.NET Core-tól (annak infrastruktúrájától és vezérlőitől) érkező elemeket az egyéni alkalmazásréteg-kódtól, az alkalmazásréteget egy külön osztálytárba is elhelyezheti, de ez nem kötelező.

A rendelési mikroszolgáltatás alkalmazásrétegkódja például közvetlenül az Ordering.API projekt (egy ASP.NET Core Web API-projekt) részeként van implementálva, a 7–23. ábrán látható módon.

Képernyőkép az Ordering.API mikroszolgáltatásról a Megoldáskezelő.

Az Ordering.API mikroszolgáltatás Megoldáskezelő nézete, amelyen az Alkalmazás mappa almappái láthatók: Viselkedések, Parancsok, DomainEventHandlers, IntegrationEvents, Models, Queries és Validations.

7–23. ábra. Az Ordering.API ASP.NET Core Web API-projekt alkalmazásrétege

ASP.NET Core tartalmaz egy egyszerű beépített IoC-tárolót (amelyet az IServiceProvider interfész képvisel), amely alapértelmezés szerint támogatja a konstruktorinjektálást, és ASP.NET bizonyos szolgáltatásokat elérhetővé tesz a DI-n keresztül. ASP.NET Core a szolgáltatás kifejezését használja a dián keresztül beszúrni kívánt összes regisztrált típushoz. Konfigurálja a beépített tároló szolgáltatásait az alkalmazás Program.cs fájljában. A függőségek olyan szolgáltatásokban vannak implementálva, amelyekre egy típusnak szüksége van, és amelyeket regisztrál az IoC-tárolóban.

Általában olyan függőségeket szeretne injektálni, amelyek infrastruktúra-objektumokat implementálnak. Az injektálandó tipikus függőség egy adattár. De bármilyen más infrastruktúra-függőséget is injektálhat. Egyszerűbb implementációk esetén közvetlenül injektálhatja a Munkaegység minta objektumot (az EF DbContext objektumot), mivel a DBContext az infrastruktúra-megőrzési objektumok implementálása is.

Az alábbi példában láthatja, hogy a .NET hogyan injektálja a szükséges adattárobjektumokat a konstruktoron keresztül. Az osztály egy parancskezelő, amelyről a következő szakaszban olvashat.

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

Az osztály az injektált adattárakat használja a tranzakció végrehajtásához és az állapotváltozások megőrzéséhez. Nem számít, hogy ez az osztály egy parancskezelő, egy ASP.NET Core Web API-vezérlő metódus vagy egy DDD Application Service. Ez végső soron egy egyszerű osztály, amely a parancskezelőhöz hasonló módon használ adattárakat, tartományi entitásokat és egyéb alkalmazáskoordinációt. A függőséginjektálás ugyanúgy működik az összes említett osztály esetében, mint a példában, amely a konstruktoron alapuló DI-t használja.

A függőségek implementálási típusainak, interfészeinek vagy absztrakcióinak regisztrálása

Mielőtt konstruktorokon keresztül injektált objektumokat használ, tudnia kell, hol kell regisztrálnia azokat az interfészeket és osztályokat, amelyek az alkalmazásosztályokba injektált objektumokat a DI-ben állítják elő. (A konstruktoron alapuló DI-hez hasonlóan, ahogy korábban is láthattuk.)

A ASP.NET Core által biztosított beépített IoC-tároló használata

Ha a ASP.NET Core által biztosított beépített IoC-tárolót használja, regisztrálja azokat a típusokat, amelyeket be szeretne szúrni a Program.cs fájlba, ahogyan az alábbi kódban is látható:

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

Az IoC-tárolók típusainak regisztrálásakor a leggyakoribb minta egy pár típus – egy interfész és annak kapcsolódó implementációs osztálya – regisztrálása. Ezután amikor objektumot kér az IoC-tárolótól bármely konstruktoron keresztül, egy bizonyos típusú interfész objektumát kéri le. Az előző példában például az utolsó sor azt állítja, hogy ha bármelyik konstruktor függ az IMyCustomRepositorytól (interfész vagy absztrakció), az IoC-tároló a MyCustomSQLServerRepository implementációs osztály egy példányát fogja injektálni.

A Scrutor-kódtár használata az automatikus típusok regisztrálásához

Ha a DI-t a .NET-ben használja, érdemes lehet beolvasni egy szerelvényt, és konvenció szerint automatikusan regisztrálni annak típusait. Ez a funkció jelenleg nem érhető el a ASP.NET Core-ban. Ehhez azonban használhatja a Scrutor-kódtárat . Ez a módszer akkor kényelmes, ha több tucat típust kell regisztrálnia az IoC-tárolóban.

További erőforrások

Az Autofac használata IoC-tárolóként

További IoC-tárolókat is használhat, és csatlakoztathatja őket a ASP.NET Core-folyamathoz, ahogy az Autofac-ot használó eShopOnContainers mikroszolgáltatás rendelésében is. Az Autofac használatakor általában modulokon keresztül regisztrálja a típusokat, amelyek lehetővé teszik a regisztrációs típusok több fájl közötti felosztását attól függően, hogy hol találhatók a típusok, ahogyan az alkalmazástípusokat több osztálykódtár között is eloszthatja.

A következők például az Ordering.API Webes API-projekt Autofac-alkalmazásmodulja az injektálandó típusokkal.

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

Az Autofac emellett egy olyan funkcióval is rendelkezik, amely beolvassa a szerelvényeket, és névegyezmények szerint regisztrálja a típusokat.

A regisztrációs folyamat és a fogalmak nagyon hasonlóak ahhoz, ahogyan a típusok regisztrálhatók a beépített ASP.NET Core IoC-tárolóval, de az Autofac használatakor a szintaxis kissé eltérő.

A példakódban az absztrakciós IOrderRepository az OrderRepository implementációs osztálysal együtt van regisztrálva. Ez azt jelenti, hogy amikor egy konstruktor függőséget deklarál az IOrderRepository absztrakción vagy interfészen keresztül, az IoC-tároló injektálja az OrderRepository osztály egy példányát.

A példány hatókörtípusa határozza meg, hogy a példányok hogyan oszthatók meg ugyanazon szolgáltatás vagy függőség kérései között. Függőségre vonatkozó kérés esetén az IoC-tároló a következőket tudja visszaadni:

  • Egyetlen példány élettartamonként (a ASP.NET Core IoC-tárolóban hatókörként hivatkozik).

  • Függőségenként egy új példány (a ASP.NET Core IoC-tárolóban átmenetiként szerepel).

  • Az IoC-tárolóval (a ASP.NET Core IoC-tárolóban singletonként) megosztott egyetlen példány az összes objektumon.

További erőforrások

A parancs- és parancskezelői minták implementálása

Az előző szakaszban bemutatott DI-through-konstruktor példában az IoC-tároló adattárakat injektált egy konstruktoron egy osztályban. De pontosan hol adták be őket? Egy egyszerű webes API-ban (például az eShopOnContainers katalógus mikroszolgáltatásában) a ASP.NET Core kérésfolyamatának részeként egy vezérlőkonstruktorba injektálja őket az MVC-vezérlők szintjén. A szakasz kezdeti kódjában (az eShopOnContainers Ordering.API szolgáltatásából származó CreateOrderCommandHandler osztályban) azonban a függőségek injektálása egy adott parancskezelő konstruktorán keresztül történik. Magyarázzuk el, hogy mi az a parancskezelő, és miért érdemes használni.

A parancsminta alapvetően a jelen útmutatóban korábban bevezetett CQRS-mintához kapcsolódik. A CQRS-nek két oldala van. Az első terület a lekérdezések, amelyek egyszerűsített lekérdezéseket használnak a Dapper mikro ORM-jével, amelyet korábban már elmagyaráztak. A második terület a parancsok, amelyek a tranzakciók kiindulópontjai, valamint a szolgáltatáson kívüli bemeneti csatorna.

Ahogy a 7–24. ábrán látható, a minta az ügyféloldali parancsok elfogadásán, a tartománymodell szabályai alapján történő feldolgozáson, valamint az állapotok tranzakciókkal való megőrzésén alapul.

Az ügyfél és az adatbázis közötti magas szintű adatfolyamot bemutató ábra.

7–24. ábra. A parancsok vagy a "tranzakciós oldal" magas szintű nézete egy CQRS-mintában

A 7–24. ábra azt mutatja, hogy a felhasználói felületi alkalmazás egy parancsot küld az API-n keresztül, amely CommandHandlera tartománymodelltől és az infrastruktúrától függ, hogy frissítse az adatbázist.

A parancsosztály

A parancsok arra kérik a rendszert, hogy hajtsa végre a rendszer állapotát módosító műveletet. A parancsok elengedhetetlenek, és csak egyszer kell feldolgozni.

Mivel a parancsok imperatívak, általában egy imperatív hangulatú igével (például "létrehozás" vagy "frissítés") nevezik el őket, és tartalmazhatják az összesítő típust, például a CreateOrderCommandot. Az eseménytől eltérően a parancs nem a múltból származó tény; ez csak egy kérelem, és így elutasítható.

A parancsok a felhasználói felületről származhatnak egy kérést kezdeményező felhasználó vagy egy folyamatkezelő eredményeképpen, ha a folyamatkezelő egy aggregátumot irányít egy művelet végrehajtására.

A parancsok egyik fontos jellemzője, hogy egyetlen fogadónak csak egyszer kell feldolgoznia. Ennek az az oka, hogy a parancsok egyetlen műveletet vagy tranzakciót kívánnak végrehajtani az alkalmazásban. Például ugyanazt a sorrendlétrehozás parancsot nem szabad többször feldolgozni. Ez fontos különbség a parancsok és az események között. Az események több alkalommal is feldolgozhatók, mert sok rendszer vagy mikroszolgáltatás érdeklődhet az esemény iránt.

Emellett fontos, hogy a parancsok csak egyszer dolgozzanak fel, ha a parancs nem idempotens. A parancs idempotens, ha többször is végrehajtható az eredmény módosítása nélkül, akár a parancs természete, akár a rendszer által a parancs kezelése miatt.

Célszerű a parancsokat és frissítéseket idempotenssé tenni, ha a tartomány üzleti szabályai és a változók értelmében van. Ha például ugyanazt a példát szeretné használni, ha bármilyen okból (újrapróbálkozási logika, hackelés stb.) ugyanaz a CreateOrder-parancs többször is eléri a rendszert, azonosítania kell azt, és gondoskodnia kell arról, hogy ne hozzon létre több rendelést. Ehhez valamilyen identitást kell csatolnia a műveletekhez, és azonosítania kell, hogy a parancs vagy a frissítés már fel lett-e dolgozva.

Parancsot küld egyetlen fogadónak; nem tesz közzé parancsot. A közzététel olyan eseményekhez készült, amelyek tényeket közölnek – hogy történt valami, és érdekes lehet az esemény fogadói számára. Események esetén a közzétevőnek nincs aggálya azzal kapcsolatban, hogy mely fogadók kapják meg az eseményt, vagy hogy mit csinálnak. A tartomány- vagy integrációs események azonban egy másik történetnek számítanak, amely már az előző szakaszokban is be van vezetve.

A parancsok olyan osztálysal implementálva lesznek, amely adatmezőket vagy gyűjteményeket tartalmaz a parancs végrehajtásához szükséges összes információval. A parancs egy speciális adatátviteli objektum (DTO), amely kifejezetten módosítások vagy tranzakciók igénylésére szolgál. Maga a parancs pontosan a parancs feldolgozásához szükséges információkon alapul, és semmi többre.

Az alábbi példa az egyszerűsített osztályt CreateOrderCommand mutatja be. Ez egy nem módosítható parancs, amelyet az eShopOnContainers rendszerbeli mikroszolgáltatás rendeléséhez használnak.

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

A parancsosztály alapvetően tartalmazza az üzleti tranzakciók végrehajtásához szükséges összes adatot a tartománymodell-objektumok használatával. A parancsok tehát egyszerűen olyan adatstruktúrák, amelyek írásvédett adatokat tartalmaznak, és nem tartalmaznak viselkedést. A parancs neve jelzi a célját. Sok nyelvben, például a C#-ban a parancsok osztályokként jelennek meg, de valódi objektumorientált értelemben nem valódi osztályok.

További jellemző, hogy a parancsok nem módosíthatók, mivel a várt használat az, hogy közvetlenül a tartománymodell dolgozza fel őket. A tervezett élettartamuk alatt nem kell változniuk. A C#-osztályban a nem módosíthatóság úgy érhető el, hogy nincs olyan halmaz vagy más metódus, amely megváltoztatja a belső állapotot.

Ne feledje, hogy ha a parancsok szerializálási/deszerializálási folyamaton mennek keresztül, a tulajdonságoknak rendelkezniük kell egy privát setterrel és a [DataMember] (vagy [JsonProperty]) attribútummal. Ellenkező esetben a deszerializáló nem tudja rekonstruálni az objektumot a célhelyen a szükséges értékekkel. Akkor is használhat igazán írásvédett tulajdonságokat, ha az osztály rendelkezik olyan konstruktorral, amely az összes tulajdonság paramétereit tartalmazza, a szokásos CamelCase elnevezési konvencióval, és a konstruktort megjegyzésekkel [JsonConstructor]adhatja meg. Ehhez a beállításhoz azonban több kódra van szükség.

A rendelések létrehozásához használt parancsosztály például valószínűleg hasonló a létrehozni kívánt sorrend adataihoz, de valószínűleg nincs szüksége ugyanazokra az attribútumokra. Például CreateOrderCommand nem rendelkezik rendelésazonosítóval, mert a rendelés még nem lett létrehozva.

Számos parancsosztály lehet egyszerű, és csak néhány mezőt igényel bizonyos állapotokról, amelyeket módosítani kell. Ez akkor lenne így, ha egy megrendelés állapotát csak a "folyamatban" állapotról "fizetős" vagy "kiszállított" állapotra módosítja az alábbihoz hasonló parancs használatával:

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

Egyes fejlesztők a felhasználói felület kérési objektumait elkülönítik a parancsDTO-któl, de ez csak a preferencia kérdése. Ez egy unalmas elkülönítés, amely nem sok további értékkel rendelkezik, és az objektumok szinte pontosan ugyanazok az alakzatok. Az eShopOnContainersben például egyes parancsok közvetlenül az ügyféloldalról érkeznek.

A Parancskezelő osztály

Minden parancshoz implementálnia kell egy adott parancskezelő osztályt. Így működik a minta, és itt fogja használni a parancsobjektumot, a tartományobjektumokat és az infrastruktúra-adattár objektumait. A parancskezelő valójában az alkalmazásréteg központja a CQRS és a DDD szempontjából. Az összes tartománylogikát azonban a tartományosztályokban kell tárolni – az összesítő gyökereken (gyökérentitásokon), gyermekentitásokon vagy tartományi szolgáltatásokon belül, de nem a parancskezelőben, amely az alkalmazásréteg osztálya.

A parancskezelő osztály erős ugródeszka az előző szakaszban említett Egyetlen felelősség elve (SRP) eléréséhez.

A parancskezelő kap egy parancsot, és a használt összesítésből szerzi be az eredményt. Az eredménynek vagy a parancs sikeres végrehajtásának vagy kivételnek kell lennie. Kivétel esetén a rendszerállapotnak változatlannak kell lennie.

A parancskezelő általában a következő lépéseket hajtja végre:

  • Megkapja a parancsobjektumot, például egy DTO-t (a mediátortól vagy más infrastruktúra-objektumtól).

  • Ellenőrzi, hogy a parancs érvényes-e (ha a mediátor nem érvényesíti).

  • Példányosítja az aktuális parancs célpéldányának számító összesítő gyökérpéldányt.

  • Végrehajtja a metódust az összesítő gyökérpéldányon, és lekéri a szükséges adatokat a parancsból.

  • Az aggregátum új állapotát megőrzi a kapcsolódó adatbázison. Ez az utolsó művelet a tényleges tranzakció.

A parancskezelő általában egyetlen aggregátummal foglalkozik, amelyet az összesítő gyökér (gyökérentitás) hajt végre. Ha egy parancs fogadása több aggregátumra is hatással van, tartományi események használatával több összesítésre is propagálja az állapotokat vagy műveleteket.

Itt az a fontos, hogy egy parancs feldolgozásakor az összes tartománylogikának a tartománymodellben (az összesítésekben) kell lennie, teljes mértékben beágyazva és készen kell állnia az egységtesztelésre. A parancskezelő csak úgy működik, hogy lekérje a tartománymodellt az adatbázisból, és végső lépésként meg kell mondania az infrastruktúrarétegnek (adattáraknak), hogy a modell módosításakor megőrizze a módosításokat. Ennek a megközelítésnek az az előnye, hogy a tartománylogikát egy izolált, teljes mértékben beágyazott, gazdag, viselkedési tartománymodellben újrabontással végezheti el anélkül, hogy módosítaná a kódot az alkalmazás- vagy infrastruktúrarétegekben, ami a vízvezeték-szint (parancskezelők, webes API, adattárak stb.).

Amikor a parancskezelők összetettek lesznek, túl sok logikával, ez kódszag lehet. Tekintse át őket, és ha megtalálja a tartománylogikát, újrabontással helyezze át a tartomány viselkedését a tartományobjektumok metódusaiba (az összesítő gyökér- és gyermekentitást).

A parancskezelő osztály példájaként az alábbi kód ugyanazt CreateOrderCommandHandler az osztályt jeleníti meg, amelyet a fejezet elején látott. Ebben az esetben a Handle metódust és a tartománymodell objektumaival/összesítéseivel végzett műveleteket is kiemeli.

public class CreateOrderCommandHandler
        : IRequestHandler<CreateOrderCommand, bool>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IIdentityService _identityService;
    private readonly IMediator _mediator;
    private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
    private readonly ILogger<CreateOrderCommandHandler> _logger;

    // Using DI to inject infrastructure persistence Repositories
    public CreateOrderCommandHandler(IMediator mediator,
        IOrderingIntegrationEventService orderingIntegrationEventService,
        IOrderRepository orderRepository,
        IIdentityService identityService,
        ILogger<CreateOrderCommandHandler> logger)
    {
        _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
        _identityService = identityService ?? throw new ArgumentNullException(nameof(identityService));
        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
        _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<bool> Handle(CreateOrderCommand message, CancellationToken cancellationToken)
    {
        // Add Integration event to clean the basket
        var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(message.UserId);
        await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStartedIntegrationEvent);

        // Add/Update the Buyer AggregateRoot
        // DDD patterns comment: Add child entities and value-objects through the Order Aggregate-Root
        // methods and constructor so validations, invariants and business logic
        // make sure that consistency is preserved across the whole aggregate
        var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode);
        var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration);

        foreach (var item in message.OrderItems)
        {
            order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Units);
        }

        _logger.LogInformation("----- Creating Order - Order: {@Order}", order);

        _orderRepository.Add(order);

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

A parancskezelőnek a következő további lépéseket kell elvégeznie:

  • A parancs adataival az összesítő gyökér metódusaival és viselkedésével működhet.

  • A tartományobjektumokon belül tartományi eseményeket hozhat létre a tranzakció végrehajtása közben, de ez parancskezelői szempontból transzparens.

  • Ha az aggregátum műveleti eredménye sikeres, és a tranzakció befejezése után integrációs eseményeket hoz létre. (Ezeket infrastruktúraosztályok, például adattárak is előhozhatják.)

További erőforrások

A parancsfolyamat folyamata: parancskezelő aktiválása

A következő kérdés a parancskezelő meghívása. Manuálisan hívhatja meg az egyes kapcsolódó ASP.NET Core-vezérlőkről. Ez a megközelítés azonban túl összekapcsolt lenne, és nem ideális.

A másik két fő lehetőség, amelyek az ajánlott lehetőségek, a következők:

  • Egy memórián belüli mediátorminta-összetevőn keresztül.

  • Aszinkron üzenetsor, vezérlők és kezelők között.

A mediátorminta (memóriában) használata a parancsfolyamatban

A 7–25. ábrán látható módon a CQRS-megközelítésben egy intelligens mediátort használ, hasonlóan a memóriában lévő buszhoz, amely elég intelligens ahhoz, hogy a kapott parancs vagy DTO típusa alapján átirányítsa a megfelelő parancskezelőt. Az összetevők közötti egyetlen fekete nyíl az objektumok közötti függőségeket (sok esetben a DI-en keresztül injektálva) a kapcsolódó interakciókkal jelöli.

Az ügyfél és az adatbázis közötti részletesebb adatfolyamot bemutató ábra.

7–25. ábra. A Mediátor minta használata folyamatban egyetlen CQRS-mikroszolgáltatásban

A fenti ábrán a 7–24. kép nagyítása látható: a ASP.NET Core vezérlő elküldi a parancsot a MediatR parancsfolyamatának, így a megfelelő kezelőhöz jutnak.

A Mediator-minta használatának az az oka, hogy a vállalati alkalmazásokban a feldolgozási kérelmek bonyolulttá tehetik a folyamatot. Nyílt számú keresztirányú problémát szeretne hozzáadni, például a naplózást, az érvényesítést, a naplózást és a biztonságot. Ezekben az esetekben egy mediátorfolyamatra (lásd : Mediátor-minta) támaszkodhat, hogy eszközöket biztosítson ezekhez az extra viselkedésekhez vagy keresztirányú problémákhoz.

A mediátor egy objektum, amely magában foglalja a folyamat "hogyanját": az állapot, a parancskezelő meghívásának módja vagy a kezelőnek megadott hasznos adat alapján koordinálja a végrehajtást. A mediátor-összetevőkkel központosított és transzparens módon alkalmazhat keresztirányú szempontokat dekorátorok (vagy folyamatok viselkedése a MediatR 3 óta). További információ: Decorator minta.

A dekorátorok és a viselkedések hasonlóak az Aspektusorientált programozáshoz (AOP), csak a mediátor összetevő által felügyelt adott folyamatra vonatkoznak. A keresztvágási szempontokat megvalósító AOP-szempontokat a fordításkor injektált aspektusszövők vagy az objektumhívások elfogása alapján alkalmazzák. A két tipikus AOP-módszerről néha azt mondják, hogy "varázslatként" működik, mert nem könnyű belátni, hogy az AOP hogyan végzi a munkáját. Ha súlyos problémákat vagy hibákat kezel, az AOP-t nehéz lehet hibakereséssel kezelni. Másrészt ezek a dekorátorok/viselkedések explicitek és csak a mediátor kontextusában vannak alkalmazva, így a hibakeresés sokkal kiszámíthatóbb és egyszerűbb.

A mikroszolgáltatást rendelő eShopOnContainersben például két mintaviselkedés, egy LogBehavior osztály és egy ValidatorBehavior osztály van implementálva . A viselkedések implementálását a következő szakaszban ismertetheti azzal, hogy bemutatja, hogyan használják az eShopOnContainers a MediatR-viselkedéseket.

Üzenetsorok használata (nem elérhető) a parancs folyamatában

Másik lehetőség az aszinkron üzenetek használata közvetítők vagy üzenetsorok alapján, a 7–26. ábrán látható módon. Ez a beállítás a mediátor összetevővel is kombinálható közvetlenül a parancskezelő előtt.

Ha üzenetsort használó adatfolyamot ábrázoló ábra.

7–26. ábra. Üzenetsorok használata (folyamaton kívüli és folyamatközi kommunikáció) CQRS-parancsokkal

A parancsfolyamatot magas rendelkezésre állású üzenetsor is képes kezelni, hogy a parancsokat a megfelelő kezelőnek kézbesítse. Ha üzenetsorokat használ a parancsok elfogadásához, azzal tovább bonyolíthatja a parancsfolyamatot, mivel a folyamatot valószínűleg két, a külső üzenetsoron keresztül csatlakoztatott folyamatra kell felosztania. Akkor is érdemes használni, ha az aszinkron üzenetkezelésen alapuló nagyobb méretezhetőségre és teljesítményre van szüksége. Vegye figyelembe, hogy a 7–26. ábra esetében a vezérlő csak közzéteszi a parancsüzenetet az üzenetsorba, és visszatér. Ezután a parancskezelők saját tempójukban dolgozzák fel az üzeneteket. Ez az üzenetsorok nagy előnye: az üzenetsor pufferként működhet olyan esetekben, amikor hiperskálázhatóságra van szükség, például részvények vagy bármely más, nagy mennyiségű bejövő adatot tartalmazó forgatókönyv esetén.

Az üzenetsorok aszinkron jellege miatt azonban ki kell találnia, hogyan kommunikálhat az ügyfélalkalmazással a parancsfolyamat sikerességéről vagy sikertelenségéről. Általában soha ne használjon "tűz és felejtés" parancsokat. Minden üzleti alkalmazásnak tudnia kell, hogy sikeresen feldolgoztak-e egy parancsot, vagy legalábbis érvényesítve és elfogadva.

Így ha egy aszinkron üzenetsorba küldött parancsüzenet érvényesítése után képes válaszolni az ügyfélre, az összetettebbé teszi a rendszert, összehasonlítva egy folyamaton belüli parancsfolyamatkal, amely a tranzakció futtatása után adja vissza a művelet eredményét. Az üzenetsorok használatával előfordulhat, hogy a parancsfolyamat eredményét más műveleti eredményüzeneteken keresztül kell visszaadnia, amelyek további összetevőket és egyéni kommunikációt igényelnek a rendszerben.

Ezenkívül az aszinkron parancsok egyirányú parancsok, amelyek sok esetben nem feltétlenül szükségesek, amint azt a Burtsev Alekszej és Greg Young közötti alábbi érdekes beszélgetés ismerteti egy online beszélgetésben:

[Burtsev Alekszej] Sok olyan kódot találok, ahol az emberek ok nélkül használnak aszinkron parancskezelést vagy egyirányú parancsüzeneteket (nem végeznek hosszú műveletet, nem hajtanak végre külső aszinkron kódot, még alkalmazásközi határt sem használnak az üzenetbusz használatához). Miért vezetik be ezt a szükségtelen bonyolultságot? És valójában nem láttam CQRS-kód példát a parancskezelők blokkolására eddig, bár a legtöbb esetben csak jól fog működni.

[Greg Young] [...] aszinkron parancs nem létezik; ez valójában egy másik esemény. Ha el kell fogadnom, amit küld, és eseményt kell előhoznom, ha nem értek egyet, akkor már nem azt mondod, hogy tegyek valamit [vagyis ez nem parancs]. Azt mondod, hogy valami történt. Ez elsőre kis különbségnek tűnik, de ennek számos következménye van.

Az aszinkron parancsok jelentősen növelik a rendszer összetettségét, mivel a hibák jelzésére nincs egyszerű mód. Ezért az aszinkron parancsok nem ajánlottak, csak akkor, ha skálázási követelményekre van szükség, vagy speciális esetekben, amikor a belső mikroszolgáltatásokat üzenetküldéssel kommunikálják. Ezekben az esetekben külön jelentési és helyreállítási rendszert kell megterveznie a hibákhoz.

Az eShopOnContainers kezdeti verziójában úgy döntöttek, hogy szinkron parancsfeldolgozást használnak, amely HTTP-kérelmekből indult és a Mediator-minta alapján lett vezérelve. Így egyszerűen visszaadhatja a folyamat sikerességét vagy sikertelenségét, akárcsak a CreateOrderCommandHandler implementációban.

Mindenesetre ennek az alkalmazás vagy a mikroszolgáltatás üzleti követelményei alapján kell döntenie.

A parancsfolyamat implementálása mediátormintával (MediatR)

Ez az útmutató minta-implementációként azt javasolja, hogy a Mediator-minta alapján a folyamatban lévő folyamatot használja a parancsbetöltési parancsok végrehajtásához, és a parancsokat a memóriában a megfelelő parancskezelőkhöz irányítsa. Az útmutató azt is javasolja, hogy alkalmazzon viselkedést annak érdekében, hogy elkülönítse a keresztirányú aggodalmakat.

A .NET-ben való implementáláshoz több nyílt forráskódú kódtár érhető el, amelyek implementálják a Mediator-mintát. Az ebben az útmutatóban használt kódtár a MediatR nyílt forráskódú könyvtára (amelyet Jimmy Bogard hozott létre), de más megközelítést is használhat. A MediatR egy kis és egyszerű kódtár, amely lehetővé teszi a memóriában lévő üzenetek, például a parancsok feldolgozását, dekorátorok vagy viselkedések alkalmazása közben.

A Mediátor minta használatával csökkentheti a csatolást, és elkülönítheti a kért munka aggodalmait, miközben automatikusan csatlakozik a munkát végző kezelőhöz – ebben az esetben a parancskezelőkhöz.

Egy másik jó ok, hogy használja a Mediátor minta magyarázta Jimmy Bogard, amikor áttekinti ezt az útmutatót:

Azt hiszem, érdemes megemlíteni tesztelés itt - ez egy szép egységes ablakot a viselkedését a rendszer. Bekérés, válasz-out. Ezt a szempontot elég értékesnek találtuk a következetesen viselkedő tesztek készítésében.

Először is tekintsünk meg egy minta WebAPI-vezérlőt, ahol ténylegesen a mediátorobjektumot használná. Ha nem a mediátorobjektumot használta, be kell szúrnia a vezérlő összes függőségét, például egy naplózó objektumot és másokat. Ezért a konstruktor bonyolult lenne. Másrészt, ha a mediátor objektumot használja, a vezérlő konstruktora sokkal egyszerűbb lehet, sok függőség helyett csak néhány függőséggel, ha keresztvágási műveletenként egy volt, mint az alábbi példában:

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

Láthatja, hogy a mediátor tiszta és sovány webes API-vezérlőkonstruktort biztosít. Emellett a vezérlőmetódusok között a parancs mediátorobjektumba küldéséhez szükséges kód majdnem egy sorból áll:

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

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

Idempotens parancsok implementálása

Az eShopOnContainersben a fentinél fejlettebb példa egy CreateOrderCommand objektum elküldése a Rendelési mikroszolgáltatásból. Mivel azonban a rendelési üzleti folyamat egy kicsit összetettebb, és esetünkben valójában a Basket mikroszolgáltatásban kezdődik, a CreateOrderCommand objektum elküldésének művelete egy UserCheckoutAcceptedIntegrationEventHandler nevű integrációs eseménykezelőből történik, nem pedig egy egyszerű WebAPI-vezérlőből, amelyet az ügyfélalkalmazásból hívunk, mint az előző egyszerűbb példában.

A parancs MediatR-nek való elküldésének művelete azonban meglehetősen hasonló, ahogy az alábbi kódban is látható.

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

Ez az eset azonban kissé fejlettebb is, mivel idempotens parancsokat is implementálunk. A CreateOrderCommand folyamatnak idempotensnek kell lennie, ezért ha ugyanaz az üzenet duplikálva lesz a hálózaton keresztül, bármilyen okból, például az újrapróbálkoztatások miatt, ugyanazt az üzleti rendelést csak egyszer dolgozza fel a rendszer.

Ez az üzleti parancs (ebben az esetben a CreateOrderCommand) burkolásával és egy általános IdentifiedCommandbe való beágyazásával valósítható meg, amelyet a hálózaton keresztül érkező összes üzenet azonosítója követ, amelynek idempotensnek kell lennie.

Az alábbi kódban láthatja, hogy az IdentifiedCommand nem más, mint egy DTO és egy azonosító, valamint a burkolt üzleti parancsobjektum.

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

Ezután a IdentifiedCommandHandler.cs nevű IdentifiedCommand commandHandlerje alapvetően ellenőrzi, hogy az üzenet részeként érkező azonosító már létezik-e egy táblában. Ha már létezik, a parancs nem lesz újra feldolgozva, ezért idempotens parancsként viselkedik. Ezt az infrastruktúra-kódot az _requestManager.ExistAsync alábbi metódushívás hajtja végre.

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

Mivel az IdentifiedCommand úgy működik, mint egy üzleti parancs borítékja, amikor az üzleti parancsot fel kell dolgoztatni, mert nem ismétlődő azonosító, akkor az adott belső üzleti parancsot veszi igénybe, és újraküldi a Mediatornak, ahogy a kód előző részében is látható, amikor fut _mediator.Send(message.Command), a IdentifiedCommandHandler.cs.

Ennek során összekapcsolja és futtatja az üzleti parancskezelőt, ebben az esetben a CreateOrderCommandHandlert, amely tranzakciókat futtat a Rendelési adatbázison, ahogyan az az alábbi kódban látható.

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

A MediatR által használt típusok regisztrálása

Ahhoz, hogy a MediatR tisztában legyen a parancskezelő osztályokkal, regisztrálnia kell a mediátorosztályokat és a parancskezelő osztályokat az IoC-tárolóban. Alapértelmezés szerint a MediatR az Autofac-ot használja IoC-tárolóként, de használhatja a beépített ASP.NET Core IoC-tárolót vagy a MediatR által támogatott egyéb tárolót is.

Az alábbi kód bemutatja, hogyan regisztrálhatja a Mediator típusait és parancsjait autofac-modulok használatakor.

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

Itt történik a "varázslat" a MediatR-vel.

Mivel az egyes parancskezelők implementálják az általános IRequestHandler<T> felületet, a szerelvények metódussal történő RegisteredAssemblyTypes regisztrálásakor az összes megjelölt IRequestHandler típus is regisztrálva lesz a saját .Commands Példa:

public class CreateOrderCommandHandler
  : IRequestHandler<CreateOrderCommand, bool>
{

Ez az a kód, amely a parancsokat a parancskezelőkkel korrelálja. A kezelő csak egy egyszerű osztály, de örökli RequestHandler<T>a parancstípust, és a MediatR gondoskodik róla, hogy a rendszer a megfelelő hasznos adattal (a paranccsal) hívja meg.

Keresztirányú aggodalmak alkalmazása a parancsok a MediatR viselkedésével való feldolgozásakor

Van még egy dolog: a közvetítői folyamatra vonatkozó keresztirányú aggodalmak alkalmazása. Az Autofac regisztrációs modul kódjának végén azt is láthatja, hogyan regisztrál egy viselkedéstípust, konkrétan egy egyéni LoggingBehavior osztályt és egy ValidatorBehavior osztályt. De más egyéni viselkedéseket is hozzáadhat.

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

A LoggingBehavior osztály a következő kódként implementálható, amely naplózza a végrehajtás alatt álló parancskezelő adatait, és azt, hogy sikeres volt-e vagy sem.

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

A viselkedési osztály implementálásával és a folyamatba való regisztrálással (a fenti MediatorModule-ban) a MediatR-ben feldolgozott összes parancs naplózási információkat fog adni a végrehajtásról.

A mikroszolgáltatást rendelő eShopOnContainers egy második viselkedést is alkalmaz az alapszintű ellenőrzésekhez, a FluentValidation kódtárra támaszkodó ValidatorBehavior osztályra, ahogyan az a következő kódban látható:

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

Itt a viselkedés kivételt okoz, ha az ellenőrzés sikertelen, de eredményobjektumot is visszaadhat, amely tartalmazza a parancs eredményét, ha az sikeres volt, vagy ha nem, akkor az érvényesítési üzeneteket. Ez valószínűleg megkönnyíti az érvényesítési eredmények megjelenítését a felhasználó számára.

Ezután a FluentValidation kódtár alapján létre kell hoznia a CreateOrderCommand által átadott adatok ellenőrzését, ahogyan az alábbi kódban is látható:

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

További érvényesítéseket is létrehozhat. Ez egy nagyon tiszta és elegáns módszer a parancsérvényesítések implementálásához.

Hasonló módon más viselkedéseket is implementálhat a parancsokra alkalmazni kívánt további szempontok vagy keresztvágási problémák esetén.

További erőforrások

A mediátorminta
A lakberendező minta
MediatR (Jimmy Bogard)
Fluent-ellenőrzés