De microservicetoepassingslaag implementeren met behulp van de web-API
Tip
Deze inhoud is een fragment uit het eBook, .NET Microservices Architecture for Containerized .NET Applications, beschikbaar op .NET Docs of als een gratis downloadbare PDF die offline kan worden gelezen.
Afhankelijkheidsinjectie gebruiken om infrastructuurobjecten in uw toepassingslaag te injecteren
Zoals eerder vermeld, kan de toepassingslaag worden geïmplementeerd als onderdeel van het artefact (assembly) dat u bouwt, zoals binnen een web-API-project of een MVC-web-app-project. In het geval van een microservice die is gebouwd met ASP.NET Core, is de toepassingslaag meestal uw web-API-bibliotheek. Als u wilt scheiden van wat afkomstig is van ASP.NET Core (de infrastructuur plus uw controllers) van uw aangepaste toepassingslaagcode, kunt u uw toepassingslaag ook in een afzonderlijke klassebibliotheek plaatsen, maar dat is optioneel.
De code van de toepassingslaag van de bestellende microservice wordt bijvoorbeeld rechtstreeks geïmplementeerd als onderdeel van het Order.API-project (een ASP.NET Core Web API-project), zoals wordt weergegeven in afbeelding 7-23.
De Solution Explorer-weergave van de Microservice Ordering.API, met de submappen onder de map Toepassing: Behaviors, Commands, DomainEventHandlers, IntegrationEvents, Models, Query's en Validaties.
Afbeelding 7-23. De toepassingslaag in het Order.API-ASP.NET Core Web API-project
ASP.NET Core bevat een eenvoudige ingebouwde IoC-container (vertegenwoordigd door de IServiceProvider-interface) die standaard constructorinjectie ondersteunt en ASP.NET bepaalde services beschikbaar maakt via DI. ASP.NET Core gebruikt de term service voor alle typen die u registreert die via DI worden geïnjecteerd. U configureert de ingebouwde containerservices in het Program.cs-bestand van uw toepassing. Uw afhankelijkheden worden geïmplementeerd in de services die een type nodig heeft en die u registreert in de IoC-container.
Normaal gesproken wilt u afhankelijkheden injecteren die infrastructuurobjecten implementeren. Een typische afhankelijkheid voor injecteren is een opslagplaats. Maar u kunt elke andere infrastructuurafhankelijkheid injecteren die u mogelijk hebt. Voor eenvoudigere implementaties kunt u het object Unit of Work-patroon (het EF DbContext-object) rechtstreeks injecteren, omdat dbContext ook de implementatie is van uw infrastructuurpersistentieobjecten.
In het volgende voorbeeld ziet u hoe .NET de vereiste opslagplaatsobjecten injecteert via de constructor. De klasse is een opdrachthandler, die in de volgende sectie wordt behandeld.
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);
}
}
De klasse gebruikt de geïnjecteerde opslagplaatsen om de transactie uit te voeren en de statuswijzigingen te behouden. Het maakt niet uit of die klasse een opdrachthandler, een ASP.NET Core Web API-controllermethode of een DDD-toepassingsservice is. Het is uiteindelijk een eenvoudige klasse die gebruikmaakt van opslagplaatsen, domeinentiteiten en andere toepassingscoördinatie op dezelfde manier als een opdrachthandler. Afhankelijkheidsinjectie werkt op dezelfde manier voor alle genoemde klassen, zoals in het voorbeeld met behulp van DI op basis van de constructor.
De implementatietypen en interfaces of abstracties van afhankelijkheden registreren
Voordat u de objecten gebruikt die zijn geïnjecteerd via constructors, moet u weten waar u de interfaces en klassen moet registreren die de objecten produceren die via DI in uw toepassingsklassen zijn geïnjecteerd. (Net als DI op basis van de constructor, zoals eerder weergegeven.)
De ingebouwde IoC-container gebruiken die wordt geleverd door ASP.NET Core
Wanneer u de ingebouwde IoC-container van ASP.NET Core gebruikt, registreert u de typen die u in het Program.cs-bestand wilt injecteren, zoals in de volgende code:
// 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>();
Het meest voorkomende patroon bij het registreren van typen in een IoC-container is het registreren van een paar typen: een interface en de bijbehorende implementatieklasse. Wanneer u vervolgens een object aanvraagt vanuit de IoC-container via een constructor, vraagt u een object van een bepaald type interface aan. In het vorige voorbeeld geeft de laatste regel bijvoorbeeld aan dat wanneer een van uw constructors afhankelijk is van IMyCustomRepository (interface of abstractie), de IoC-container een exemplaar van de MyCustomSQLServerRepository-implementatieklasse injecteert.
De Scrutor-bibliotheek gebruiken voor automatische registratie van typen
Wanneer u DI in .NET gebruikt, wilt u misschien een assembly scannen en de typen automatisch op conventie registreren. Deze functie is momenteel niet beschikbaar in ASP.NET Core. U kunt hiervoor echter de Scrutor-bibliotheek gebruiken. Deze aanpak is handig wanneer u tientallen typen hebt die moeten worden geregistreerd in uw IoC-container.
Aanvullende bronnen
Matthew King. Services registreren bij Scrutor
https://www.mking.net/blog/registering-services-with-scrutorDe Hellang van Den. Scrutor. GitHub-opslagplaats.
https://github.com/khellang/Scrutor
Autofac gebruiken als een IoC-container
U kunt ook extra IoC-containers gebruiken en deze aansluiten op de ASP.NET Core-pijplijn, zoals in de bestellende microservice in eShopOnContainers, die gebruikmaakt van Autofac. Wanneer u Autofac gebruikt, registreert u doorgaans de typen via modules, zodat u de registratietypen tussen meerdere bestanden kunt splitsen, afhankelijk van waar uw typen zich bevinden, net zoals de toepassingstypen kunnen worden gedistribueerd over meerdere klassebibliotheken.
Hier volgt bijvoorbeeld de module Autofac-toepassing voor het project Ordering.API Web API met de typen die u wilt injecteren.
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 heeft ook een functie voor het scannen van assembly's en het registreren van typen op naamconventies.
Het registratieproces en de concepten zijn vergelijkbaar met de manier waarop u typen kunt registreren bij de ingebouwde ASP.NET Core IoC-container, maar de syntaxis bij het gebruik van Autofac is iets anders.
In de voorbeeldcode wordt de abstractie IOrderRepository geregistreerd samen met de implementatieklasse OrderRepository. Dit betekent dat wanneer een constructor een afhankelijkheid declareren via de IOrderRepository-abstractie of -interface, de IoC-container een exemplaar van de klasse OrderRepository injecteert.
Het bereiktype exemplaar bepaalt hoe een exemplaar wordt gedeeld tussen aanvragen voor dezelfde service of afhankelijkheid. Wanneer een aanvraag voor een afhankelijkheid wordt ingediend, kan de IoC-container het volgende retourneren:
Eén exemplaar per levensduurbereik (waarnaar wordt verwezen in de ASP.NET Core IoC-container als scoped).
Een nieuw exemplaar per afhankelijkheid (waarnaar wordt verwezen in de ASP.NET Core IoC-container als tijdelijk).
Eén exemplaar dat wordt gedeeld voor alle objecten met behulp van de IoC-container (waarnaar wordt verwezen in de ASP.NET Core IoC-container als singleton).
Aanvullende bronnen
Inleiding tot afhankelijkheidsinjectie in ASP.NET Core
https://learn.microsoft.com/aspnet/core/fundamentals/dependency-injectionAutofac. Officiële documentatie.
https://docs.autofac.org/en/latest/Vergelijking van de levensduur van ASP.NET Core IoC-containerservice met Autofac IoC-containerinstantiebereiken - Caesar de la Torre.
https://devblogs.microsoft.com/cesardelatorre/comparing-asp-net-core-ioc-service-life-times-and-autofac-ioc-instance-scopes/
De opdracht- en opdrachthandlerpatronen implementeren
In het voorbeeld van de DI-through-constructor dat in de vorige sectie wordt weergegeven, injecteerde de IoC-container opslagplaatsen via een constructor in een klasse. Maar precies waar werden ze geïnjecteerd? In een eenvoudige web-API (bijvoorbeeld de catalogusmicroservice in eShopOnContainers), injecteert u deze op het niveau van de MVC-controllers, in een controllerconstructor, als onderdeel van de aanvraagpijplijn van ASP.NET Core. In de eerste code van deze sectie (de klasse CreateOrderCommandHandler van de Ordering.API-service in eShopOnContainers) wordt de injectie van afhankelijkheden echter uitgevoerd via de constructor van een bepaalde opdrachthandler. Laten we uitleggen wat een opdrachthandler is en waarom u deze wilt gebruiken.
Het opdrachtpatroon is intrinsiek gerelateerd aan het CQRS-patroon dat eerder in deze handleiding is geïntroduceerd. CQRS heeft twee kanten. Het eerste gebied is query's, met behulp van vereenvoudigde query's met de Dapper micro ORM, die eerder werd uitgelegd. Het tweede gebied is opdrachten, het startpunt voor transacties en het invoerkanaal van buiten de service.
Zoals wordt weergegeven in afbeelding 7-24, is het patroon gebaseerd op het accepteren van opdrachten van de clientzijde, het verwerken ervan op basis van de domeinmodelregels en ten slotte het behouden van de statussen met transacties.
Afbeelding 7-24. Weergave op hoog niveau van de opdrachten of 'transactionele zijde' in een CQRS-patroon
In afbeelding 7-24 ziet u dat de UI-app een opdracht verzendt via de API die naar een CommandHandler
, die afhankelijk is van het domeinmodel en de infrastructuur, om de database bij te werken.
De opdrachtklasse
Een opdracht is een aanvraag voor het systeem om een actie uit te voeren waarmee de status van het systeem wordt gewijzigd. Opdrachten zijn imperatief en moeten slechts één keer worden verwerkt.
Omdat opdrachten imperatieven zijn, worden ze meestal benoemd met een werkwoord in de imperatieve stemming (bijvoorbeeld 'maken' of 'bijwerken'),) en kunnen ze het statistische type bevatten, zoals CreateOrderCommand. In tegenstelling tot een gebeurtenis is een opdracht geen feit uit het verleden; het is slechts een verzoek en kan dus worden geweigerd.
Opdrachten kunnen afkomstig zijn van de gebruikersinterface als gevolg van een gebruiker die een aanvraag initieert, of van een procesbeheerder wanneer de procesmanager een aggregatie omleiden om een actie uit te voeren.
Een belangrijk kenmerk van een opdracht is dat deze slechts één keer door één ontvanger moet worden verwerkt. Dit komt doordat een opdracht één actie of transactie is die u in de toepassing wilt uitvoeren. Dezelfde opdracht voor het maken van orders mag bijvoorbeeld niet meer dan één keer worden verwerkt. Dit is een belangrijk verschil tussen opdrachten en gebeurtenissen. Gebeurtenissen kunnen meerdere keren worden verwerkt, omdat veel systemen of microservices mogelijk geïnteresseerd zijn in de gebeurtenis.
Daarnaast is het belangrijk dat een opdracht slechts eenmaal wordt verwerkt voor het geval de opdracht niet idempotent is. Een opdracht is idempotent als deze meerdere keren kan worden uitgevoerd zonder het resultaat te wijzigen, hetzij vanwege de aard van de opdracht, of vanwege de manier waarop het systeem de opdracht verwerkt.
Het is een goede gewoonte om uw opdrachten te maken en idempotent bij te werken wanneer dit zinvol is onder de bedrijfsregels en invarianten van uw domein. Als u bijvoorbeeld hetzelfde voorbeeld wilt gebruiken, als om welke reden dan ook (pogingslogica, hacking, enzovoort) dezelfde Opdracht CreateOrder meerdere keren bereikt, moet u deze kunnen identificeren en ervoor zorgen dat u niet meerdere orders maakt. Hiervoor moet u een soort identiteit aan de bewerkingen koppelen en bepalen of de opdracht of update al is verwerkt.
U verzendt een opdracht naar één ontvanger; u publiceert geen opdracht. Publiceren is bedoeld voor gebeurtenissen die een feit aangeven, dat er iets is gebeurd en mogelijk interessant is voor gebeurtenisontvangers. In het geval van gebeurtenissen heeft de uitgever geen zorgen over welke ontvangers de gebeurtenis krijgen of wat ze doen. Maar domein- of integratie-gebeurtenissen zijn een ander verhaal dat al in eerdere secties is geïntroduceerd.
Een opdracht wordt geïmplementeerd met een klasse die gegevensvelden of verzamelingen bevat met alle informatie die nodig is om die opdracht uit te voeren. Een opdracht is een speciaal type Data Transfer Object (DTO), een die specifiek wordt gebruikt om wijzigingen of transacties aan te vragen. De opdracht zelf is gebaseerd op precies de informatie die nodig is voor het verwerken van de opdracht en niets meer.
In het volgende voorbeeld ziet u de vereenvoudigde CreateOrderCommand
klasse. Dit is een onveranderbare opdracht die wordt gebruikt in de microservice bestellen in eShopOnContainers.
// DDD and CQRS patterns comment: Note that it is recommended to implement immutable Commands
// In this case, its immutability is achieved by having all the setters as private
// plus only being able to update the data just once, when creating the object through its constructor.
// References on Immutable Commands:
// http://cqrs.nu/Faq
// https://docs.spine3.org/motivation/immutability.html
// http://blog.gauffin.org/2012/06/griffin-container-introducing-command-support/
// https://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; }
}
}
In principe bevat de opdrachtklasse alle gegevens die u nodig hebt voor het uitvoeren van een zakelijke transactie met behulp van de domeinmodelobjecten. Opdrachten zijn dus gewoon gegevensstructuren die alleen-lezengegevens bevatten en geen gedrag. De naam van de opdracht geeft het doel aan. In veel talen, zoals C#, worden opdrachten weergegeven als klassen, maar ze zijn geen echte klassen in de echte objectgeoriënteerde zin.
Als extra kenmerk zijn opdrachten onveranderbaar, omdat het verwachte gebruik is dat ze rechtstreeks door het domeinmodel worden verwerkt. Ze hoeven niet te veranderen tijdens hun verwachte levensduur. In een C#-klasse kan onveranderbaarheid worden bereikt door geen setters of andere methoden te hebben die de interne status wijzigen.
Houd er rekening mee dat als u van plan bent of verwacht dat opdrachten een serialisatie-/deserialisatieproces doorlopen, de eigenschappen een persoonlijke setter en het [DataMember]
(of [JsonProperty]
) kenmerk moeten hebben. Anders kan de deserializer het object niet met de vereiste waarden op de bestemming reconstrueren. U kunt ook echt alleen-lezen eigenschappen gebruiken als de klasse een constructor heeft met parameters voor alle eigenschappen, met de gebruikelijke naamconventie van camelCase en aantekeningen toevoegen aan de constructor als [JsonConstructor]
. Voor deze optie is echter meer code vereist.
De opdrachtklasse voor het maken van een order is bijvoorbeeld waarschijnlijk vergelijkbaar met de volgorde die u wilt maken, maar u hebt waarschijnlijk niet dezelfde kenmerken nodig. Heeft bijvoorbeeld CreateOrderCommand
geen order-id, omdat de order nog niet is gemaakt.
Veel opdrachtklassen kunnen eenvoudig zijn, waarbij slechts een paar velden nodig zijn over een bepaalde status die moet worden gewijzigd. Dat zou het geval zijn als u alleen de status van een order wijzigt van 'in proces' in 'betaald' of 'verzonden' met behulp van een opdracht die vergelijkbaar is met de volgende:
[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; }
}
Sommige ontwikkelaars maken hun UI-aanvraagobjecten gescheiden van hun opdracht-DTU's, maar dat is slechts een kwestie van voorkeur. Het is een tijdrovende scheiding met niet veel extra waarde, en de objecten zijn bijna precies dezelfde vorm. In eShopOnContainers komen sommige opdrachten bijvoorbeeld rechtstreeks van de clientzijde.
De klasse Opdrachthandler
U moet voor elke opdracht een specifieke opdrachthandlerklasse implementeren. Dat is hoe het patroon werkt en hier gebruikt u het opdrachtobject, de domeinobjecten en de opslagplaatsobjecten voor de infrastructuur. De opdrachthandler is in feite het hart van de toepassingslaag in termen van CQRS en DDD. Alle domeinlogica moet echter zijn opgenomen in de domeinklassen, binnen de statistische hoofdmappen (hoofdentiteiten), onderliggende entiteiten of domeinservices, maar niet in de opdrachthandler, een klasse uit de toepassingslaag.
De opdrachthandlerklasse biedt een sterke stap voor het bereiken van het Single Responsibility Principle (SRP) dat in een vorige sectie is vermeld.
Een opdrachthandler ontvangt een opdracht en haalt een resultaat op van de statistische functie die wordt gebruikt. Het resultaat moet de uitvoering van de opdracht of een uitzondering zijn. In het geval van een uitzondering moet de systeemstatus ongewijzigd blijven.
De opdrachthandler voert meestal de volgende stappen uit:
Het ontvangt het opdrachtobject, zoals een DTO (van de bemiddelaar of een ander infrastructuurobject).
Het controleert of de opdracht geldig is (indien niet gevalideerd door de bemiddelaar).
Hiermee wordt het geaggregeerde hoofdexemplaren geïnstitueert dat het doel is van de huidige opdracht.
De methode wordt uitgevoerd op het geaggregeerde hoofdexemplaren, zodat de vereiste gegevens uit de opdracht worden opgehaald.
De nieuwe status van de statistische functie blijft behouden voor de bijbehorende database. Deze laatste bewerking is de werkelijke transactie.
Normaal gesproken behandelt een opdrachthandler één aggregaties die wordt aangestuurd door de hoofdmap (hoofdentiteit). Als meerdere aggregaties worden beïnvloed door de ontvangst van één opdracht, kunt u domeingebeurtenissen gebruiken om statussen of acties over meerdere aggregaties door te geven.
Het belangrijkste punt hier is dat wanneer een opdracht wordt verwerkt, alle domeinlogica zich binnen het domeinmodel (de aggregaties), volledig ingekapseld en gereed voor eenheidstests. De opdrachthandler fungeert als een manier om het domeinmodel op te halen uit de database en als laatste stap om de infrastructuurlaag (opslagplaatsen) te laten weten dat de wijzigingen behouden blijven wanneer het model wordt gewijzigd. Het voordeel van deze benadering is dat u de domeinlogica kunt herstructureren in een geïsoleerd, volledig ingekapseld, uitgebreid, gedragsdomeinmodel zonder code in de toepassings- of infrastructuurlagen te wijzigen, wat het niveau van het loodgieterniveau is (opdrachthandlers, web-API, opslagplaatsen, enzovoort).
Wanneer opdrachthandlers complex worden, met te veel logica, kan dat een codegeur zijn. Controleer deze en als u domeinlogica vindt, herstructureer dan de code om dat domeingedrag te verplaatsen naar de methoden van de domeinobjecten (de geaggregeerde hoofd- en onderliggende entiteit).
Als voorbeeld van een opdrachthandlerklasse toont de volgende code dezelfde CreateOrderCommandHandler
klasse die u aan het begin van dit hoofdstuk hebt gezien. In dit geval worden ook de handle-methode en de bewerkingen met de domeinmodelobjecten/aggregaties gemarkeerd.
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);
}
}
Dit zijn aanvullende stappen die een opdrachthandler moet uitvoeren:
Gebruik de gegevens van de opdracht om te werken met de methoden en het gedrag van de statistische hoofdmap.
Intern binnen de domeinobjecten genereert u domeingebeurtenissen terwijl de transactie wordt uitgevoerd, maar dat is transparant vanuit een opdrachthandler.
Als het resultaat van de samenvoegingsbewerking is geslaagd en de transactie is voltooid, moet u integratiegebeurtenissen genereren. (Deze kunnen ook worden verhoogd door infrastructuurklassen zoals opslagplaatsen.)
Aanvullende bronnen
Mark Seemann. Aan de grenzen zijn toepassingen niet objectgeoriënteerd
https://blog.ploeh.dk/2011/05/31/AttheBoundaries,ApplicationsareNotObject-Oriented/Opdrachten en gebeurtenissen
https://cqrs.nu/faq/Command%20and%20EventsWat doet een opdrachthandler?
https://cqrs.nu/faq/Command%20HandlersJimmy Bogard. Domeinopdrachtpatronen - Handlers
https://jimmybogard.com/domain-command-patterns-handlers/Jimmy Bogard. Domeinopdrachtpatronen - validatie
https://jimmybogard.com/domain-command-patterns-validation/
De pijplijn voor het opdrachtproces: een opdrachthandler activeren
De volgende vraag is hoe u een opdrachthandler aanroept. U kunt deze handmatig aanroepen vanaf elke gerelateerde ASP.NET Core-controller. Deze benadering zou echter te gekoppeld zijn en is niet ideaal.
De andere twee hoofdopties, die de aanbevolen opties zijn, zijn:
Via een in-memory bemiddelaarpatroonartefact.
Met een asynchrone berichtenwachtrij tussen controllers en handlers.
Het bemiddelaarpatroon (in-memory) gebruiken in de opdrachtpijplijn
Zoals wordt weergegeven in afbeelding 7-25, gebruikt u in een CQRS-benadering een intelligente bemiddelaar, vergelijkbaar met een in-memory bus, die slim genoeg is om om te leiden naar de juiste opdrachthandler op basis van het type opdracht of DTO dat wordt ontvangen. De enkele zwarte pijlen tussen onderdelen vertegenwoordigen de afhankelijkheden tussen objecten (in veel gevallen geïnjecteerd via DI) met hun gerelateerde interacties.
Afbeelding 7-25. Het bemiddelaarpatroon in proces gebruiken in één CQRS-microservice
In het bovenstaande diagram ziet u een inzoombewerking van afbeelding 7-24: de ASP.NET Core-controller verzendt de opdracht naar de opdrachtpijplijn van MediatR, zodat ze bij de juiste handler komen.
De reden dat het gebruik van het bemiddelaarpatroon zinvol is, is dat de verwerkingsaanvragen in bedrijfstoepassingen ingewikkeld kunnen worden. U wilt een open aantal kruislingse problemen kunnen toevoegen, zoals logboekregistratie, validaties, controle en beveiliging. In deze gevallen kunt u vertrouwen op een bemiddelaarpijplijn (zie Bemiddelaarpatroon) om een middel te bieden voor dit extra gedrag of kruislingse zorgen.
Een bemiddelaar is een object dat de 'hoe' van dit proces inkapselt: het coördineert de uitvoering op basis van de status, de manier waarop een opdrachthandler wordt aangeroepen of de nettolading die u aan de handler opgeeft. Met een bemiddelaaronderdeel kunt u kruislingse zorgen toepassen op een gecentraliseerde en transparante manier door decorators (of pijplijngedrag sinds MediatR 3) toe te passen. Zie het Decorator-patroon voor meer informatie.
Decorators en gedragingen zijn vergelijkbaar met Aspect Oriented Programming (AOP), alleen toegepast op een specifieke procespijplijn die wordt beheerd door het bemiddelaaronderdeel. Aspecten in AOP die kruislingse zorgen implementeren, worden toegepast op basis van aspectververs die tijdens de compilatie worden geïnjecteerd of op basis van interceptie van objectoproepen. Beide typische AOP-benaderingen worden soms gezegd om 'als magie' te werken, omdat het niet gemakkelijk is om te zien hoe AOP zijn werk doet. Bij het oplossen van ernstige problemen of bugs kan AOP lastig zijn om fouten op te sporen. Aan de andere kant zijn deze decorators/gedragingen expliciet en alleen toegepast in de context van de bemiddelaar, dus foutopsporing is veel voorspelbaarder en gemakkelijk.
In de eShopOnContainers bestellende microservice heeft bijvoorbeeld een implementatie van twee voorbeeldgedragen, een LogBehavior-klasse en een ValidatorBehavior-klasse . De implementatie van het gedrag wordt uitgelegd in de volgende sectie door te laten zien hoe eShopOnContainers gebruikmaakt van MediatR-gedrag.
Berichtenwachtrijen (out-of-proc) gebruiken in de pijplijn van de opdracht
Een andere keuze is het gebruik van asynchrone berichten op basis van brokers of berichtenwachtrijen, zoals wordt weergegeven in afbeelding 7-26. Deze optie kan ook worden gecombineerd met het bemiddelaaronderdeel vlak voor de commandohandler.
Afbeelding 7-26. Berichtenwachtrijen gebruiken (buiten het proces en communicatie tussen processen) met CQRS-opdrachten
De pijplijn van de opdracht kan ook worden verwerkt door een berichtenwachtrij met hoge beschikbaarheid om de opdrachten aan de juiste handler te leveren. Het gebruik van berichtenwachtrijen om de opdrachten te accepteren, kan de pijplijn van uw opdracht verder bemoeilijken, omdat u de pijplijn waarschijnlijk moet splitsen in twee processen die zijn verbonden via de wachtrij voor externe berichten. Het moet echter worden gebruikt als u de schaalbaarheid en prestaties moet verbeteren op basis van asynchrone berichten. Houd er rekening mee dat in het geval van afbeelding 7-26 de controller alleen het opdrachtbericht in de wachtrij plaatst en retourneert. Vervolgens verwerken de opdrachthandlers de berichten in hun eigen tempo. Dat is een groot voordeel van wachtrijen: de berichtenwachtrij kan fungeren als een buffer in gevallen waarin hyperschaalbaarheid nodig is, zoals voor aandelen of een ander scenario met een groot aantal inkomende gegevens.
Vanwege de asynchrone aard van berichtenwachtrijen moet u echter bepalen hoe u kunt communiceren met de clienttoepassing over het slagen of mislukken van het proces van de opdracht. In de regel moet u nooit 'fire and forget'-opdrachten gebruiken. Elke zakelijke toepassing moet weten of een opdracht is verwerkt, of ten minste gevalideerd en geaccepteerd.
Het reageren op de client na het valideren van een opdrachtbericht dat is verzonden naar een asynchrone wachtrij, voegt dus complexiteit toe aan uw systeem, in vergelijking met een in-process opdrachtproces dat het resultaat van de bewerking retourneert na het uitvoeren van de transactie. Met behulp van wachtrijen moet u mogelijk het resultaat van het opdrachtproces retourneren via andere berichten met bewerkingsresultaten. Hiervoor zijn extra onderdelen en aangepaste communicatie in uw systeem vereist.
Daarnaast zijn asynchrone opdrachten eenrichtingsopdrachten, die in veel gevallen niet nodig zijn, zoals wordt uitgelegd in de volgende interessante uitwisseling tussen Burtsev Alexey en Greg Young in een onlinegesprek:
[Burtsev Alexey] Ik vind veel code waarbij mensen asynchrone opdrachtafhandeling of eenrichtingsopdrachtberichten gebruiken zonder reden om dit te doen (ze voeren geen lange bewerking uit, ze voeren geen externe asynchrone code uit, ze doen zelfs geen grens tussen toepassingen om berichtenbus te gebruiken). Waarom introduceren ze deze onnodige complexiteit? En eigenlijk heb ik nog geen CQRS-codevoorbeeld gezien met blokkerende opdrachthandlers, hoewel het in de meeste gevallen prima werkt.
[Greg Young] [...] er bestaat geen asynchrone opdracht; Het is eigenlijk een andere gebeurtenis. Als ik moet accepteren wat je mij stuurt en een gebeurtenis opwerp als ik het niet eens ben, dan zeg je me niet meer iets te doen [dat wil zeggen, het is geen opdracht]. Je zegt me dat er iets is gebeurd. Dit lijkt in het begin een klein verschil, maar het heeft veel gevolgen.
Asynchrone opdrachten vergroten de complexiteit van een systeem aanzienlijk, omdat er geen eenvoudige manier is om fouten aan te geven. Daarom worden asynchrone opdrachten niet aanbevolen, behalve wanneer schaalvereisten nodig zijn of in speciale gevallen bij het communiceren van de interne microservices via berichten. In die gevallen moet u een afzonderlijk rapportage- en herstelsysteem ontwerpen voor fouten.
In de eerste versie van eShopOnContainers werd besloten om synchrone opdrachtverwerking te gebruiken, gestart op basis van HTTP-aanvragen en aangestuurd door het bemiddelaarpatroon. Hierdoor kunt u eenvoudig het succes of falen van het proces retourneren, zoals in de implementatie CreateOrderCommandHandler .
In elk geval moet dit een beslissing zijn op basis van de bedrijfsvereisten van uw toepassing of microservice.
De pijplijn voor het opdrachtproces implementeren met een bemiddelaarpatroon (MediatR)
Als voorbeeld van een implementatie stelt deze handleiding voor het gebruik van de pijplijn in het proces op basis van het bemiddelaarpatroon om opdrachtopname en routeopdrachten, in het geheugen, naar de juiste opdrachthandlers te sturen. De handleiding stelt ook voor om gedrag toe te passen om kruislingse problemen te scheiden.
Voor implementatie in .NET zijn er meerdere opensource-bibliotheken beschikbaar waarmee het bemiddelaarpatroon wordt geïmplementeerd. De bibliotheek die in deze handleiding wordt gebruikt, is de opensource-bibliotheek van MediatR (gemaakt door Jimmy Bogard), maar u kunt een andere benadering gebruiken. MediatR is een kleine en eenvoudige bibliotheek waarmee u berichten in het geheugen kunt verwerken, zoals een opdracht, terwijl u decorators of gedrag toepast.
Met behulp van het bemiddelaarpatroon kunt u koppeling verminderen en de zorgen van het aangevraagde werk isoleren, terwijl u automatisch verbinding maakt met de handler die dat werk uitvoert, in dit geval aan opdrachthandlers.
Een andere goede reden om het bemiddelaarpatroon te gebruiken, werd uitgelegd door Jimmy Bogard bij het bekijken van deze handleiding:
Ik denk dat het misschien de moeite waard is om het testen hier te vermelden - het biedt een mooi consistent venster in het gedrag van uw systeem. Aanvraag-in, antwoord-out. We hebben vastgesteld dat aspect heel waardevol is in het bouwen van consistent gedragende tests.
Laten we eerst eens kijken naar een voorbeeld van een WebAPI-controller waarin u het bemiddelaarobject daadwerkelijk zou gebruiken. Als u het bemiddelaarobject niet gebruikt, moet u alle afhankelijkheden voor die controller injecteren, zoals een logboekobject en anderen. Daarom zou de constructor ingewikkeld zijn. Als u daarentegen het bemiddelaarobject gebruikt, kan de constructor van uw controller veel eenvoudiger zijn, met slechts een paar afhankelijkheden in plaats van veel afhankelijkheden als u er één per kruislingse bewerking had, zoals in het volgende voorbeeld:
public class MyMicroserviceController : Controller
{
public MyMicroserviceController(IMediator mediator,
IMyMicroserviceQueries microserviceQueries)
{
// ...
}
}
U kunt zien dat de bemiddelaar een schone en lean web-API-controllerconstructor biedt. Bovendien is binnen de controllermethoden de code voor het verzenden van een opdracht naar het bemiddelaarobject bijna één regel:
[Route("new")]
[HttpPost]
public async Task<IActionResult> ExecuteBusinessOperation([FromBody]RunOpCommand
runOperationCommand)
{
var commandResult = await _mediator.SendAsync(runOperationCommand);
return commandResult ? (IActionResult)Ok() : (IActionResult)BadRequest();
}
idempotente opdrachten implementeren
In eShopOnContainers verzendt een geavanceerder voorbeeld dan hierboven een CreateOrderCommand-object vanuit de ordermicroservice. Maar omdat het bedrijfsproces bestellen iets complexer is en in ons geval het daadwerkelijk in de Basket-microservice begint, wordt deze actie van het verzenden van het object CreateOrderCommand uitgevoerd vanaf een integratiegebeurtenishandler met de naam UserCheckoutAcceptedIntegrationEventHandler in plaats van een eenvoudige WebAPI-controller die vanuit de client-app wordt aangeroepen, zoals in het vorige eenvoudigere voorbeeld.
Niettemin is de actie voor het indienen van de opdracht bij MediatR vrij vergelijkbaar, zoals wordt weergegeven in de volgende code.
var createOrderCommand = new CreateOrderCommand(eventMsg.Basket.Items,
eventMsg.UserId, eventMsg.City,
eventMsg.Street, eventMsg.State,
eventMsg.Country, eventMsg.ZipCode,
eventMsg.CardNumber,
eventMsg.CardHolderName,
eventMsg.CardExpiration,
eventMsg.CardSecurityNumber,
eventMsg.CardTypeId);
var requestCreateOrder = new IdentifiedCommand<CreateOrderCommand,bool>(createOrderCommand,
eventMsg.RequestId);
result = await _mediator.Send(requestCreateOrder);
Dit geval is echter ook iets geavanceerder omdat we ook idempotente opdrachten implementeren. Het proces CreateOrderCommand moet idempotent zijn, dus als hetzelfde bericht wordt gedupliceerd via het netwerk, om welke reden dan ook, zoals nieuwe pogingen, wordt dezelfde bedrijfsorder slechts één keer verwerkt.
Dit wordt geïmplementeerd door de bedrijfsopdracht (in dit geval CreateOrderCommand) te verpakken en in te sluiten in een algemene IdentifiedCommand, die wordt bijgehouden door een id van elk bericht dat via het netwerk komt dat idempotent moet zijn.
In de onderstaande code ziet u dat de IdentifiedCommand niets meer is dan een DTO met en id plus het verpakte bedrijfsopdrachtobject.
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;
}
}
Vervolgens controleert de CommandHandler voor de IdentifiedCommand met de naam IdentifiedCommandHandler.cs in feite of de id die als onderdeel van het bericht komt, al in een tabel voorkomt. Als deze al bestaat, wordt die opdracht niet opnieuw verwerkt, dus gedraagt deze zich als een idempotente opdracht. Deze infrastructuurcode wordt uitgevoerd door de _requestManager.ExistAsync
onderstaande methode-aanroep.
// 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);
}
}
}
}
Omdat de IdentifiedCommand fungeert als de envelop van een zakelijke opdracht, wanneer de zakelijke opdracht moet worden verwerkt omdat het geen herhaalde id is, wordt die interne zakelijke opdracht gebruikt en opnieuw verzonden naar Bemiddelaar, zoals in het laatste deel van de hierboven weergegeven code, _mediator.Send(message.Command)
vanaf de IdentifiedCommandHandler.cs.
Wanneer u dit doet, wordt de business command handler gekoppeld en uitgevoerd, in dit geval de CreateOrderCommandHandler, die transacties uitvoert voor de orderdatabase, zoals wordt weergegeven in de volgende code.
// CreateOrderCommandHandler.cs
public class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, bool>
{
private readonly IOrderRepository _orderRepository;
private readonly IIdentityService _identityService;
private readonly IMediator _mediator;
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
private readonly ILogger<CreateOrderCommandHandler> _logger;
// Using DI to inject infrastructure persistence Repositories
public CreateOrderCommandHandler(IMediator mediator,
IOrderingIntegrationEventService orderingIntegrationEventService,
IOrderRepository orderRepository,
IIdentityService identityService,
ILogger<CreateOrderCommandHandler> logger)
{
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
_identityService = identityService ?? throw new ArgumentNullException(nameof(identityService));
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<bool> Handle(CreateOrderCommand message, CancellationToken cancellationToken)
{
// Add Integration event to clean the basket
var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(message.UserId);
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStartedIntegrationEvent);
// Add/Update the Buyer AggregateRoot
// DDD patterns comment: Add child entities and value-objects through the Order Aggregate-Root
// methods and constructor so validations, invariants and business logic
// make sure that consistency is preserved across the whole aggregate
var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode);
var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration);
foreach (var item in message.OrderItems)
{
order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Units);
}
_logger.LogInformation("----- Creating Order - Order: {@Order}", order);
_orderRepository.Add(order);
return await _orderRepository.UnitOfWork
.SaveEntitiesAsync(cancellationToken);
}
}
De typen registreren die door MediatR worden gebruikt
Als u wilt dat MediatR op de hoogte is van uw opdrachthandlerklassen, moet u de bemiddelaarklassen en de opdrachthandlerklassen registreren in uw IoC-container. MediatR maakt standaard gebruik van Autofac als de IoC-container, maar u kunt ook de ingebouwde ASP.NET Core IoC-container of een andere container gebruiken die wordt ondersteund door MediatR.
De volgende code laat zien hoe u de typen en opdrachten van Bemiddelaar registreert bij het gebruik van Autofac-modules.
public class MediatorModule : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly)
.AsImplementedInterfaces();
// Register all the Command classes (they implement IRequestHandler)
// in assembly holding the Commands
builder.RegisterAssemblyTypes(typeof(CreateOrderCommand).GetTypeInfo().Assembly)
.AsClosedTypesOf(typeof(IRequestHandler<,>));
// Other types registration
//...
}
}
Hier gebeurt 'de magie' met MediatR.
Wanneer elke opdrachthandler de algemene IRequestHandler<T>
interface implementeert, worden bij het registreren van de assembly's alle RegisteredAssemblyTypes
typen die zijn gemarkeerd als IRequestHandler
ook geregistreerd bij hun Commands
. Voorbeeld:
public class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, bool>
{
Dat is de code die opdrachten correleert met opdrachthandlers. De handler is slechts een eenvoudige klasse, maar wordt overgenomen van RequestHandler<T>
, waarbij T het opdrachttype is en MediatR zorgt ervoor dat deze wordt aangeroepen met de juiste nettolading (de opdracht).
Kruislingse problemen toepassen bij het verwerken van opdrachten met het gedrag in MediatR
Er is nog één ding: het kunnen toepassen van kruislingse zorgen op de bemiddelaarpijplijn. U kunt ook aan het einde van de moduleCode voor autofac zien hoe een gedragstype wordt geregistreerd, met name een aangepaste klasse LoggingBehavior en een ValidatorBehavior-klasse. Maar u kunt ook andere aangepaste gedragingen toevoegen.
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<,>));
}
}
Die LoggingBehavior-klasse kan worden geïmplementeerd als de volgende code, waarmee informatie wordt vastgelegd over de opdrachthandler die wordt uitgevoerd en of deze wel of niet is geslaagd.
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;
}
}
Door deze gedragsklasse te implementeren en door deze te registreren in de pijplijn (in de BemiddelaarModule hierboven), worden alle opdrachten die via MediatR worden verwerkt, informatie over de uitvoering geregistreerd.
De eShopOnContainers bestellen microservice past ook een tweede gedrag toe voor basisvalidaties, de ValidatorBehavior-klasse die afhankelijk is van de FluentValidation-bibliotheek , zoals wordt weergegeven in de volgende code:
public class ValidatorBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
{
private readonly IValidator<TRequest>[] _validators;
public ValidatorBehavior(IValidator<TRequest>[] validators) =>
_validators = validators;
public async Task<TResponse> Handle(TRequest request,
RequestHandlerDelegate<TResponse> next)
{
var failures = _validators
.Select(v => v.Validate(request))
.SelectMany(result => result.Errors)
.Where(error => error != null)
.ToList();
if (failures.Any())
{
throw new OrderingDomainException(
$"Command Validation Errors for type {typeof(TRequest).Name}",
new ValidationException("Validation exception", failures));
}
var response = await next();
return response;
}
}
Hier wordt een uitzondering gegenereerd als de validatie mislukt, maar u kunt ook een resultaatobject retourneren dat het opdrachtresultaat bevat als het is geslaagd of de validatieberichten in het geval dat dat niet het geval is. Dit maakt het waarschijnlijk gemakkelijker om validatieresultaten weer te geven aan de gebruiker.
Vervolgens maakt u op basis van de FluentValidation-bibliotheek validatie voor de gegevens die zijn doorgegeven met CreateOrderCommand, zoals in de volgende code:
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderCommandValidator()
{
RuleFor(command => command.City).NotEmpty();
RuleFor(command => command.Street).NotEmpty();
RuleFor(command => command.State).NotEmpty();
RuleFor(command => command.Country).NotEmpty();
RuleFor(command => command.ZipCode).NotEmpty();
RuleFor(command => command.CardNumber).NotEmpty().Length(12, 19);
RuleFor(command => command.CardHolderName).NotEmpty();
RuleFor(command => command.CardExpiration).NotEmpty().Must(BeValidExpirationDate).WithMessage("Please specify a valid card expiration date");
RuleFor(command => command.CardSecurityNumber).NotEmpty().Length(3);
RuleFor(command => command.CardTypeId).NotEmpty();
RuleFor(command => command.OrderItems).Must(ContainOrderItems).WithMessage("No order items found");
}
private bool BeValidExpirationDate(DateTime dateTime)
{
return dateTime >= DateTime.UtcNow;
}
private bool ContainOrderItems(IEnumerable<OrderItemDTO> orderItems)
{
return orderItems.Any();
}
}
U kunt extra validaties maken. Dit is een zeer schone en elegante manier om uw opdrachtvalidaties te implementeren.
Op een vergelijkbare manier kunt u ander gedrag implementeren voor aanvullende aspecten of kruislingse problemen die u wilt toepassen op opdrachten bij het verwerken ervan.
Aanvullende bronnen
Het bemiddelaarpatroon
- Bemiddelaarpatroon
https://en.wikipedia.org/wiki/Mediator_pattern
Het decoratorpatroon
- Decoratorpatroon
https://en.wikipedia.org/wiki/Decorator_pattern
MediatR (Jimmy Bogard)
MediatR. GitHub-opslagplaats.
https://github.com/jbogard/MediatRCQRS met MediatR en AutoMapper
https://lostechies.com/jimmybogard/2015/05/05/cqrs-with-mediatr-and-automapper/Zet uw controllers op een dieet: POST's en opdrachten.
https://lostechies.com/jimmybogard/2013/12/19/put-your-controllers-on-a-diet-posts-and-commands/Kruislingse problemen aanpakken met een bemiddelaarpijplijn
https://lostechies.com/jimmybogard/2014/09/09/tackling-cross-cutting-concerns-with-a-mediator-pipeline/CQRS en REST: de perfecte match
https://lostechies.com/jimmybogard/2016/06/01/cqrs-and-rest-the-perfect-match/Voorbeelden van mediatR-pijplijnen
https://lostechies.com/jimmybogard/2016/10/13/mediatr-pipeline-examples/Verticale segmenttestarmaturen voor MediatR en ASP.NET Core
https://lostechies.com/jimmybogard/2016/10/24/vertical-slice-test-fixtures-for-mediatr-and-asp-net-core/MediatR-extensies voor afhankelijkheidsinjectie van Microsoft uitgebracht
https://lostechies.com/jimmybogard/2016/07/19/mediatr-extensions-for-microsoft-dependency-injection-released/
Fluent-validatie
- Jeremy Skinner. FluentValidation. GitHub-opslagplaats.
https://github.com/JeremySkinner/FluentValidation