Not
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Dricks
Det här innehållet är ett utdrag från eBook, .NET Microservices Architecture for Containerized .NET Applications, tillgängligt på .NET Docs eller som en kostnadsfri nedladdningsbar PDF som kan läsas offline.
Använda beroendeinmatning för att mata in infrastrukturobjekt i programlagret
Som tidigare nämnts kan programskiktet implementeras som en del av den artefakt (sammansättning) som du skapar, till exempel i ett webb-API-projekt eller ett MVC-webbappsprojekt. När det gäller en mikrotjänst som skapats med ASP.NET Core är programskiktet vanligtvis ditt webb-API-bibliotek. Om du vill separera vad som kommer från ASP.NET Core (dess infrastruktur plus dina kontrollanter) från din anpassade programlagerkod kan du även placera programlagret i ett separat klassbibliotek, men det är valfritt.
Till exempel implementeras programlagerkoden för den beställande mikrotjänsten direkt som en del av projektet Ordering.API (ett ASP.NET Core Web API-projekt), enligt bild 7–23.
Solution Explorer-vyn för mikrotjänsten Ordering.API som visar undermapparna under programmappen: Beteenden, kommandon, DomainEventHandlers, IntegrationEvents, modeller, frågor och valideringar.
Bild 7-23. Programlagret i projektet Ordering.API ASP.NET Core Web API
ASP.NET Core innehåller en enkel inbyggd IoC-container (representerad av gränssnittet IServiceProvider) som stöder konstruktorinmatning som standard och ASP.NET gör vissa tjänster tillgängliga via DI. ASP.NET Core använder termen tjänst för någon av de typer som du registrerar som ska matas in via DI. Du konfigurerar de inbyggda containertjänsterna i programmets Program.cs-fil . Dina beroenden implementeras i de tjänster som en typ behöver och som du registrerar i IoC-containern.
Vanligtvis vill du mata in beroenden som implementerar infrastrukturobjekt. Ett typiskt beroende att mata in är en lagringsplats. Men du kan mata in alla andra infrastrukturberoenden som du kan ha. För enklare implementeringar kan du direkt mata in ditt arbetsenhetsmönsterobjekt (EF DbContext-objektet), eftersom DBContext också är implementeringen av infrastrukturbeständighetsobjekten.
I följande exempel kan du se hur .NET matar in de nödvändiga lagringsplatsens objekt via konstruktorn. Klassen är en kommandohanterare som beskrivs i nästa avsnitt.
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);
}
}
Klassen använder de inmatade lagringsplatserna för att köra transaktionen och spara tillståndsändringarna. Det spelar ingen roll om den klassen är en kommandohanterare, en ASP.NET Core Web API-kontrollantmetod eller en DDD-programtjänst. Det är i slutändan en enkel klass som använder lagringsplatser, domänentiteter och annan programsamordning på ett sätt som liknar en kommandohanterare. Beroendeinmatning fungerar på samma sätt för alla nämnda klasser, som i exemplet med DI baserat på konstruktorn.
Registrera beroendeimplementeringstyper och gränssnitt eller abstraktioner
Innan du använder objekten som matas in via konstruktorer måste du veta var du ska registrera de gränssnitt och klasser som producerar objekten som matas in i dina programklasser via DI. (Precis som DI baserat på konstruktorn, som du visade tidigare.)
Använd den inbyggda IoC-containern som tillhandahålls av ASP.NET Core
När du använder den inbyggda IoC-containern som tillhandahålls av ASP.NET Core registrerar du de typer som du vill mata in i Program.cs-filen, som i följande kod:
// 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>();
Det vanligaste mönstret när du registrerar typer i en IoC-container är att registrera ett par typer – ett gränssnitt och dess relaterade implementeringsklass. När du sedan begär ett objekt från IoC-containern via en konstruktor begär du ett objekt av en viss typ av gränssnitt. I föregående exempel anger till exempel den sista raden att när någon av dina konstruktorer har ett beroende av IMyCustomRepository (gränssnitt eller abstraktion) matar IoC-containern in en instans av implementeringsklassen MyCustomSQLServerRepository.
Använd Scrutor-biblioteket för automatisk typregistrering
När du använder DI i .NET kanske du vill kunna skanna en sammansättning och automatiskt registrera dess typer enligt konvention. Den här funktionen är för närvarande inte tillgänglig i ASP.NET Core. Du kan dock använda Scrutor-biblioteket för det. Den här metoden är praktisk när du har dussintals typer som måste registreras i din IoC-container.
Ytterligare resurser
Matthew King. Registrera tjänster med Scrutor
https://www.mking.net/blog/registering-services-with-scrutorKristian Hellang. Scrutor. GitHub-lagringsplats.
https://github.com/khellang/Scrutor
Använda Autofac som en IoC-container
Du kan också använda ytterligare IoC-containrar och ansluta dem till ASP.NET Core-pipelinen, som i beställningsmikrotjänsten i eShopOnContainers, som använder Autofac. När du använder Autofac registrerar du vanligtvis typerna via moduler, vilket gör att du kan dela upp registreringstyperna mellan flera filer beroende på var dina typer finns, precis som du kan ha programtyperna fördelade över flera klassbibliotek.
Följande är till exempel programmodulen Autofac för web-API-projektet Ordering.API med de typer som du vill mata in.
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 har också en funktion för att skanna sammansättningar och registrera typer efter namnkonventioner.
Registreringsprocessen och begreppen liknar hur du kan registrera typer med den inbyggda ASP.NET Core IoC-containern, men syntaxen när du använder Autofac är lite annorlunda.
I exempelkoden registreras abstraktionen IOrderRepository tillsammans med implementeringsklassen OrderRepository. Det innebär att när en konstruktor deklarerar ett beroende via abstraktionen eller gränssnittet för IOrderRepository matar IoC-containern in en instans av klassen OrderRepository.
Instansomfångstypen avgör hur en instans delas mellan begäranden för samma tjänst eller beroende. När en begäran görs för ett beroende kan IoC-containern returnera följande:
En enskild instans per livslängdsomfång (refereras till i ASP.NET Core IoC-containern som omfång).
En ny instans per beroende (som i ASP.NET Core IoC-containern kallas tillfälligt).
En enda instans som delas över alla objekt med hjälp av IoC-containern (som i ASP.NET Core IoC-containern kallas singleton).
Ytterligare resurser
Introduktion till beroendeinmatning i ASP.NET Core
https://learn.microsoft.com/aspnet/core/fundamentals/dependency-injectionAutofac. Officiell dokumentation.
https://docs.autofac.org/en/latest/Jämföra ASP.NET Kärn-IoC-containertjänstlivslängder med Autofac IoC-containerinstansomfång – Cesar de la Torre.
https://devblogs.microsoft.com/cesardelatorre/comparing-asp-net-core-ioc-service-life-times-and-autofac-ioc-instance-scopes/
Implementera kommando- och kommandohanterarmönstren
I exemplet DI-through-constructor som visades i föregående avsnitt injicerade IoC-containern lagringsplatser via en konstruktor i en klass. Men exakt var injicerades de? I ett enkelt webb-API (till exempel katalogmikrotjänsten i eShopOnContainers) matar du in dem på MVC-kontrollanternas nivå, i en styrenhetskonstruktor, som en del av pipelinen för begäran för ASP.NET Core. I den första koden i det här avsnittet ( klassen CreateOrderCommandHandler från Tjänsten Ordering.API i eShopOnContainers) görs dock inmatningen av beroenden via konstruktorn för en viss kommandohanterare. Låt oss förklara vad en kommandohanterare är och varför du vill använda den.
Kommandomönstret är i sig relaterat till CQRS-mönstret som introducerades tidigare i den här guiden. CQRS har två sidor. Det första området är frågor, med hjälp av förenklade frågor med Dapper micro ORM, som förklarades tidigare. Det andra området är kommandon, som är startpunkten för transaktioner, och indatakanalen utanför tjänsten.
Som visas i bild 7-24 baseras mönstret på att acceptera kommandon från klientsidan, bearbeta dem baserat på domänmodellreglerna och slutligen bevara tillstånden med transaktioner.
Bild 7-24. Vy på hög nivå av kommandona eller "transaktionssidan" i ett CQRS-mönster
Bild 7–24 visar att användargränssnittsappen skickar ett kommando via API:et som kommer till en CommandHandler, som är beroende av domänmodellen och infrastrukturen för att uppdatera databasen.
Kommandoklassen
Ett kommando är en begäran om att systemet ska utföra en åtgärd som ändrar systemets tillstånd. Kommandon är absolut nödvändiga och bör bearbetas bara en gång.
Eftersom kommandon är imperativa namnges de vanligtvis med ett verb i imperativt humör (till exempel "skapa" eller "uppdatera" och de kan innehålla aggregeringstypen, till exempel CreateOrderCommand. Till skillnad från en händelse är ett kommando inte ett faktum från det förflutna. Det är bara en begäran och kan därför avslås.
Kommandon kan komma från användargränssnittet som ett resultat av att en användare initierar en begäran eller från en processhanterare när processhanteraren dirigerar en aggregering för att utföra en åtgärd.
En viktig egenskap för ett kommando är att det bara ska bearbetas en gång av en enda mottagare. Det beror på att ett kommando är en enda åtgärd eller transaktion som du vill utföra i programmet. Till exempel bör samma orderskapandekommando inte bearbetas mer än en gång. Det här är en viktig skillnad mellan kommandon och händelser. Händelser kan bearbetas flera gånger, eftersom många system eller mikrotjänster kan vara intresserade av händelsen.
Dessutom är det viktigt att ett kommando endast bearbetas en gång om kommandot inte är idempotent. Ett kommando är idempotent om det kan köras flera gånger utan att ändra resultatet, antingen på grund av kommandots natur eller på grund av hur systemet hanterar kommandot.
Det är en bra idé att göra dina kommandon och uppdateringar idempotent när det är meningsfullt enligt domänens affärsregler och invarianter. Om du till exempel vill använda samma exempel, om samma CreateOrder-kommando av någon anledning (omförsökslogik, hackning osv.) når systemet flera gånger, bör du kunna identifiera det och se till att du inte skapar flera beställningar. För att göra det måste du koppla någon form av identitet i åtgärderna och identifiera om kommandot eller uppdateringen redan har bearbetats.
Du skickar ett kommando till en enda mottagare. du inte publicerar något kommando. Publicering är för händelser som anger ett faktum – att något har hänt och kan vara intressant för händelsemottagare. Vid händelser har utgivaren inga problem med vilka mottagare som får händelsen eller vad de gör. Men domän- eller integrationshändelser är en annan historia som redan har introducerats i tidigare avsnitt.
Ett kommando implementeras med en klass som innehåller datafält eller samlingar med all information som behövs för att köra kommandot. Ett kommando är en särskild typ av dataöverföringsobjekt (DTO), ett som specifikt används för att begära ändringar eller transaktioner. Själva kommandot baseras på exakt den information som behövs för att bearbeta kommandot, och inget mer.
I följande exempel visas den förenklade CreateOrderCommand klassen. Det här är ett oföränderligt kommando som används i beställningsmikrotjänsten i 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; }
}
}
I grund och botten innehåller kommandoklassen alla data som du behöver för att utföra en affärstransaktion med hjälp av domänmodellobjekten. Därför är kommandon helt enkelt datastrukturer som innehåller skrivskyddade data och inget beteende. Kommandots namn anger dess syfte. På många språk som C# representeras kommandon som klasser, men de är inte sanna klasser i verklig objektorienterad mening.
Som ytterligare en egenskap är kommandon oföränderliga, eftersom den förväntade användningen är att de bearbetas direkt av domänmodellen. De behöver inte ändras under den beräknade livslängden. I en C#-klass kan du uppnå oföränderlighet genom att inte ha några setters eller andra metoder som ändrar det interna tillståndet.
Tänk på att om du tänker eller förväntar dig att kommandon ska gå igenom en serialiserings-/deserialiseringsprocess måste egenskaperna ha en privat setter och [DataMember] attributet (eller [JsonProperty]). Annars kommer deserialiseraren inte att kunna rekonstruera objektet på målet med de nödvändiga värdena. Du kan också använda skrivskyddade egenskaper om klassen har en konstruktor med parametrar för alla egenskaper, med den vanliga namngivningskonventionen för camelCase och kommenterar konstruktorn som [JsonConstructor]. Det här alternativet kräver dock mer kod.
Kommandoklassen för att skapa en order liknar till exempel förmodligen data i den ordning som du vill skapa, men du behöver förmodligen inte samma attribut. Det finns till exempel CreateOrderCommand inget order-ID eftersom ordern inte har skapats ännu.
Många kommandoklasser kan vara enkla och kräver bara ett fåtal fält om något tillstånd som behöver ändras. Det skulle vara fallet om du bara ändrar statusen för en order från "pågående" till "betald" eller "levererad" med hjälp av ett kommando som liknar följande:
[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; }
}
Vissa utvecklare gör sina gränssnittsbegärandeobjekt åtskilda från sina kommando-DTU:er, men det är bara en fråga om inställningar. Det är en omständlig separation med inte mycket ytterligare värde, och objekten är nästan exakt samma form. I eShopOnContainers kommer till exempel vissa kommandon direkt från klientsidan.
Kommandohanterarklassen
Du bör implementera en specifik kommandohanterarklass för varje kommando. Det är så mönstret fungerar och det är där du ska använda kommandoobjektet, domänobjekten och infrastrukturlagringsplatsens objekt. Kommandohanteraren är i själva verket kärnan i programskiktet när det gäller CQRS och DDD. All domänlogik bör dock finnas i domänklasserna – inom de aggregerade rötterna (rotentiteter), underordnade entiteter eller domäntjänster, men inte i kommandohanteraren, som är en klass från programskiktet.
Kommandohanterarklassen erbjuder en stark språngbräda i vägen för att uppnå principen om enskilt ansvar (SRP) som nämns i ett tidigare avsnitt.
En kommandohanterare tar emot ett kommando och hämtar ett resultat från den mängd som används. Resultatet bör antingen vara en lyckad körning av kommandot eller ett undantag. I händelse av ett undantag bör systemtillståndet vara oförändrat.
Kommandohanteraren utför vanligtvis följande steg:
Det tar emot kommandoobjektet, till exempel en DTO (från medlaren eller annat infrastrukturobjekt).
Det verifierar att kommandot är giltigt (om det inte verifieras av medlaren).
Den instans av den aggregerade rotinstansen som är målet för det aktuella kommandot.
Den kör metoden på den aggregerade rotinstansen och hämtar nödvändiga data från kommandot.
Den bevarar det nya tillståndet för aggregeringen till den relaterade databasen. Den senaste åtgärden är den faktiska transaktionen.
Vanligtvis hanterar en kommandohanterare en enda aggregering som drivs av dess aggregerade rot (rotentitet). Om flera aggregeringar ska påverkas av mottagningen av ett enda kommando kan du använda domänhändelser för att sprida tillstånd eller åtgärder över flera aggregeringar.
Den viktiga punkten här är att när ett kommando bearbetas bör all domänlogik finnas i domänmodellen (aggregeringarna), helt inkapslad och redo för enhetstestning. Kommandohanteraren fungerar bara som ett sätt att hämta domänmodellen från databasen och som det sista steget be infrastrukturlagret (lagringsplatserna) att spara ändringarna när modellen ändras. Fördelen med den här metoden är att du kan omstrukturera domänlogiken i en isolerad, helt inkapslad, omfattande, beteendemässig domänmodell utan att ändra kod i program- eller infrastrukturskikten, som är VVS-nivå (kommandohanterare, webb-API, lagringsplatser osv.).
När kommandohanterare blir komplexa, med för mycket logik, kan det vara en kodlukt. Granska dem och om du hittar domänlogik omstrukturerar du koden för att flytta domänbeteendet till metoderna för domänobjekten (den aggregerade roten och den underordnade entiteten).
Som ett exempel på en kommandohanterarklass visar följande kod samma CreateOrderCommandHandler klass som du såg i början av det här kapitlet. I det här fallet markeras även metoden Handle och åtgärderna med domänmodellobjekten/aggregeringarna.
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);
}
}
Det här är ytterligare steg som en kommandohanterare bör vidta:
Använd kommandots data för att arbeta med den aggregerade rotens metoder och beteende.
Internt inom domänobjekten genererar du domänhändelser medan transaktionen körs, men det är transparent från kommandohanterarsynpunkt.
Om aggregeringens åtgärdsresultat lyckas och när transaktionen har slutförts genererar du integreringshändelser. (Dessa kan också genereras av infrastrukturklasser som lagringsplatser.)
Ytterligare resurser
Mark Seemann. Vid gränserna är program inte objektorienterade
https://blog.ploeh.dk/2011/05/31/AttheBoundaries,Applikationer är inte objektorienterade/Kommandon och händelser
https://cqrs.nu/faq/Command%20and%20EventsVad gör en kommandohanterare?
https://cqrs.nu/faq/Command%20HandlersJimmy Bogard. Domänkommandomönster – hanterare
https://jimmybogard.com/domain-command-patterns-handlers/Jimmy Bogard. Domänkommandomönster – validering
https://jimmybogard.com/domain-command-patterns-validation/
Kommandoprocesspipelinen: så här utlöser du en kommandohanterare
Nästa fråga är hur du anropar en kommandohanterare. Du kan anropa den manuellt från varje relaterad ASP.NET Core-styrenhet. Detta tillvägagångssätt skulle dock vara för kopplat och är inte idealiskt.
De andra två huvudalternativen, som är de rekommenderade alternativen, är:
Genom en minnesintern mediatormönsterartefakt.
Med en asynkron meddelandekö, mellan kontrollanter och hanterare.
Använd Mediator-mönstret (minnesinternt) i kommandopipelinen
Som du ser i bild 7–25 använder du i en CQRS-metod en intelligent medlare som liknar en minnesintern buss, som är tillräckligt smart för att omdirigera till rätt kommandohanterare baserat på vilken typ av kommando eller DTO som tas emot. De enkla svarta pilarna mellan komponenterna representerar beroendena mellan objekt (i många fall inmatade via DI) med deras relaterade interaktioner.
Bild 7-25. Använda mediatormönstret i en enda CQRS-mikrotjänst
Diagrammet ovan visar en zoomning från bild 7–24: ASP.NET Core-kontrollanten skickar kommandot till MediatR:s kommandopipeline, så att de kommer till rätt hanterare.
Anledningen till att det är vettigt att använda mediatormönstret är att bearbetningsbegäranden kan bli komplicerade i företagsprogram. Du vill kunna lägga till ett öppet antal övergripande problem som loggning, validering, granskning och säkerhet. I dessa fall kan du förlita dig på en medlarpipeline (se Mediator-mönster) för att tillhandahålla ett sätt för dessa extra beteenden eller övergripande problem.
En medlare är ett objekt som kapslar in "hur" i den här processen: det samordnar körning baserat på tillstånd, hur en kommandohanterare anropas eller den nyttolast som du anger för hanteraren. Med en medlarkomponent kan du tillämpa övergripande problem på ett centraliserat och transparent sätt genom att tillämpa dekoratörer (eller pipelinebeteenden sedan MediatR 3). Mer information finns i mönstret Dekoratör.
Dekoratörer och beteenden liknar aspektorienterad programmering (AOP) och tillämpas endast på en specifik processpipeline som hanteras av medlarkomponenten. Aspekter i AOP som implementerar övergripande problem tillämpas baserat på aspektvävare som matas in vid kompileringstid eller baseras på avlyssning av objektanrop. Båda typiska AOP-metoder sägs ibland fungera "som magi", eftersom det inte är lätt att se hur AOP gör sitt arbete. När du hanterar allvarliga problem eller buggar kan AOP vara svårt att felsöka. Å andra sidan är dessa dekoratörer/beteenden explicita och tillämpas endast i samband med medlaren, så felsökning är mycket mer förutsägbart och enkelt.
I eShopOnContainers som beställer mikrotjänster finns till exempel en implementering av två exempelbeteenden, en LogBehavior-klass och en ValidatorBehavior-klass . Implementeringen av beteenden förklaras i nästa avsnitt genom att visa hur eShopOnContainers använder MediatR-beteenden.
Använda meddelandeköer (out-of-proc) i kommandots pipeline
Ett annat alternativ är att använda asynkrona meddelanden baserade på asynkrona meddelanden baserat på asynkrona meddelandeköer eller meddelandeköer, som visas i bild 7–26. Det alternativet kan också kombineras med medlarkomponenten precis före kommandohanteraren.
Bild 7-26. Använda meddelandeköer (ur processen och kommunikation mellan processer) med CQRS-kommandon
Kommandots pipeline kan också hanteras av en meddelandekö med hög tillgänglighet för att leverera kommandona till lämplig hanterare. Att använda meddelandeköer för att acceptera kommandona kan ytterligare komplicera kommandots pipeline, eftersom du förmodligen behöver dela upp pipelinen i två processer som är anslutna via den externa meddelandekön. Ändå bör den användas om du behöver ha bättre skalbarhet och prestanda baserat på asynkrona meddelanden. Tänk på att när det gäller bild 7–26 skickar kontrollanten bara kommandomeddelandet i kön och returnerar. Sedan bearbetar kommandohanterarna meddelandena i sin egen takt. Det är en stor fördel med köer: meddelandekön kan fungera som en buffert när hyperskalbarhet behövs, till exempel för aktier eller andra scenarion med en hög mängd inkommande data.
Men på grund av meddelandeköernas asynkrona karaktär måste du ta reda på hur du kommunicerar med klientprogrammet om att kommandots process lyckades eller misslyckades. Som regel bör du aldrig använda kommandona "eld och glöm". Varje affärsprogram måste veta om ett kommando har bearbetats eller åtminstone verifierats och godkänts.
Att kunna svara på klienten efter att ha verifierat ett kommandomeddelande som skickades till en asynkron kö ökar därför komplexiteten i systemet jämfört med en processbaserad kommandoprocess som returnerar åtgärdens resultat efter att transaktionen har körts. Med hjälp av köer kan du behöva returnera resultatet av kommandoprocessen via andra åtgärdsresultatmeddelanden, vilket kräver ytterligare komponenter och anpassad kommunikation i systemet.
Dessutom är asynkrona kommandon enkelriktade kommandon, som i många fall kanske inte behövs, vilket förklaras i följande intressanta utbyte mellan Burtsev Alexey och Greg Young i en onlinekonversation:
[Burtsev Alexey] Jag hittar massor av kod där människor använder asynkron kommandohantering eller enkelriktade kommandomeddelanden utan anledning att göra det (de gör inte någon lång åtgärd, de kör inte extern asynkron kod, de inte ens korsprogramgräns för att använda meddelandebuss). Varför inför de den här onödiga komplexiteten? Och faktiskt har jag inte sett ett CQRS-kodexempel med blockering av kommandohanterare hittills, även om det kommer att fungera bra i de flesta fall.
[Greg Young] [...] ett asynkront kommando finns inte. det är faktiskt en annan händelse. Om jag måste acceptera vad du skickar mig och väcka en händelse om jag inte håller med, är det inte längre du som säger till mig att göra något [det vill säga, det är inte ett kommando]. Det är du som säger att något har gjorts. Detta verkar vara en liten skillnad i början, men det har många konsekvenser.
Asynkrona kommandon ökar avsevärt komplexiteten i ett system, eftersom det inte finns något enkelt sätt att indikera fel. Därför rekommenderas asynkrona kommandon inte annat än när skalningskrav behövs eller i särskilda fall när de interna mikrotjänsterna kommuniceras via meddelanden. I sådana fall måste du utforma ett separat rapporterings- och återställningssystem för fel.
I den första versionen av eShopOnContainers bestämdes det att använda synkron kommandobearbetning, som startades från HTTP-begäranden och drevs av Mediator-mönstret. Det gör att du enkelt kan returnera processens framgång eller misslyckande, som i Implementeringen av CreateOrderCommandHandler .
I vilket fall som helst bör detta vara ett beslut baserat på programmets eller mikrotjänstens affärskrav.
Implementera kommandoprocesspipelinen med ett mediatormönster (MediatR)
Som ett exempel på implementering föreslår den här guiden att du använder den pågående pipelinen baserat på Mediator-mönstret för att köra kommandoinmatnings- och routningskommandon, i minnet, till rätt kommandohanterare. Guiden föreslår också tillämpning av beteenden för att separera övergripande problem.
För implementering i .NET finns det flera bibliotek med öppen källkod som implementerar Mediator-mönstret. Biblioteket som används i den här guiden är MediatR-biblioteket med öppen källkod (skapat av Jimmy Bogard), men du kan använda en annan metod. MediatR är ett litet och enkelt bibliotek som gör att du kan bearbeta minnesinterna meddelanden som ett kommando, samtidigt som du tillämpar dekoratörer eller beteenden.
Med hjälp av Mediator-mönstret kan du minska kopplingen och isolera oron för det begärda arbetet, samtidigt som du automatiskt ansluter till den hanterare som utför det arbetet – i det här fallet till kommandohanterare.
En annan bra anledning att använda Mediator-mönstret förklarades av Jimmy Bogard när han granskade den här guiden:
Jag tror att det kan vara värt att nämna testning här - det ger ett trevligt konsekvent fönster i beteendet i ditt system. Begärande in, svar ut. Vi har funnit att den aspekten är ganska värdefull när det gäller att skapa konsekvent beter sig tester.
Först ska vi titta på en WebAPI-exempelstyrenhet där du faktiskt skulle använda medlarobjektet. Om du inte använde medlarobjektet skulle du behöva mata in alla beroenden för kontrollanten, till exempel ett loggningsobjekt och andra. Konstruktorn skulle därför vara komplicerad. Om du däremot använder medlarobjektet kan konstruktorn för kontrollanten vara mycket enklare, med bara några beroenden i stället för många beroenden om du hade en per korsskärningsåtgärd, som i följande exempel:
public class MyMicroserviceController : Controller
{
public MyMicroserviceController(IMediator mediator,
IMyMicroserviceQueries microserviceQueries)
{
// ...
}
}
Du kan se att medlaren tillhandahåller en ren och mager webb-API-styrenhetskonstruktor. Inom kontrollantmetoderna är dessutom koden för att skicka ett kommando till medlarobjektet nästan en rad:
[Route("new")]
[HttpPost]
public async Task<IActionResult> ExecuteBusinessOperation([FromBody]RunOpCommand
runOperationCommand)
{
var commandResult = await _mediator.SendAsync(runOperationCommand);
return commandResult ? (IActionResult)Ok() : (IActionResult)BadRequest();
}
Implementera idempotentkommandon
I eShopOnContainers skickar ett mer avancerat exempel än ovan ett CreateOrderCommand-objekt från mikrotjänsten Ordering. Men eftersom orderaffärsprocessen är lite mer komplex och i vårt fall börjar den i basketmikrotjänsten, den här åtgärden för att skicka Objektet CreateOrderCommand utförs från en integrationshändelsehanterare med namnet UserCheckoutAcceptedIntegrationEventHandler i stället för en enkel WebAPI-styrenhet som anropas från klientappen som i föregående enklare exempel.
Åtgärden att skicka kommandot till MediatR är dock ganska lik, vilket visas i följande kod.
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);
Men det här fallet är också något mer avancerat eftersom vi även implementerar idempotent-kommandon. CreateOrderCommand-processen bör vara idempotent, så om samma meddelande dupliceras via nätverket, på grund av någon anledning, till exempel återförsök, bearbetas samma affärsorder bara en gång.
Detta implementeras genom att omsluta affärskommandot (i det här fallet CreateOrderCommand) och bädda in det i en allmän IdentifiedCommand, som spåras av ett ID för varje meddelande som kommer via nätverket som måste vara idempotent.
I koden nedan kan du se att IdentifiedCommand inte är något annat än en DTO med och ID plus det omslutna affärskommandoobjektet.
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;
}
}
Sedan kontrollerar CommandHandler för IdentifiedCommand med namnet IdentifiedCommandHandler.cs i princip om ID:t som kommer som en del av meddelandet redan finns i en tabell. Om det redan finns bearbetas inte kommandot igen, så det fungerar som ett idempotent-kommando. Infrastrukturkoden utförs av metodanropet _requestManager.ExistAsync nedan.
// 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);
}
}
}
}
Eftersom IdentifiedCommand fungerar som ett företagskommandos kuvert, när affärskommandot behöver bearbetas eftersom det inte är ett upprepat ID, tar det det inre affärskommandot och skicka det till Mediator igen, som i den sista delen av koden som visas ovan när du kör _mediator.Send(message.Command), från IdentifiedCommandHandler.cs.
När du gör det länkas och körs affärskommandohanteraren, i det här fallet CreateOrderCommandHandler, som kör transaktioner mot beställningsdatabasen, enligt följande kod.
// 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);
}
}
Registrera de typer som används av MediatR
För att MediatR ska kunna känna till dina kommandohanterarklasser måste du registrera medlarklasserna och kommandohanterarklasserna i din IoC-container. MediatR använder som standard Autofac som IoC-container, men du kan också använda den inbyggda ASP.NET Core IoC-containern eller någon annan container som stöds av MediatR.
Följande kod visar hur du registrerar Mediators typer och kommandon när du använder Autofac-moduler.
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
//...
}
}
Det är här "magin händer" med MediatR.
När varje kommandohanterare implementerar det allmänna IRequestHandler<T> gränssnittet, när du registrerar sammansättningarna med hjälp av RegisteredAssemblyTypes metoden registreras alla typer som markerats som IRequestHandler också med deras Commands. Till exempel:
public class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, bool>
{
Det är koden som korrelerar kommandon med kommandohanterare. Hanteraren är bara en enkel klass, men den ärver från RequestHandler<T>, där T är kommandotypen, och MediatR ser till att den anropas med rätt nyttolast (kommandot).
Tillämpa övergripande problem vid bearbetning av kommandon med beteenden i MediatR
Det finns ytterligare en sak: att kunna tillämpa övergripande problem på medlarpipelinen. Du kan också se i slutet av autofac-registreringsmodulkoden hur den registrerar en beteendetyp, särskilt en anpassad LoggingBehavior-klass och en ValidatorBehavior-klass. Men du kan också lägga till andra anpassade beteenden.
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<,>));
}
}
Klassen LoggingBehavior kan implementeras som följande kod, som loggar information om kommandohanteraren som körs och om den lyckades eller inte.
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;
}
}
Bara genom att implementera den här beteendeklassen och genom att registrera den i pipelinen (i MediatorModule ovan) kommer alla kommandon som bearbetas via MediatR att logga information om körningen.
EShopOnContainers som beställer mikrotjänster tillämpar också ett andra beteende för grundläggande valideringar, klassen ValidatorBehavior som förlitar sig på FluentValidation-biblioteket , enligt följande kod:
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;
}
}
Här skapar beteendet ett undantag om verifieringen misslyckas, men du kan också returnera ett resultatobjekt som innehåller kommandoresultatet om det lyckades eller valideringsmeddelandena om det inte gjorde det. Detta skulle förmodligen göra det enklare att visa valideringsresultat för användaren.
Sedan, baserat på FluentValidation-biblioteket , skapar du verifiering för de data som skickas med CreateOrderCommand, som i följande kod:
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();
}
}
Du kan skapa ytterligare valideringar. Det här är ett mycket rent och elegant sätt att implementera dina kommandovalideringar.
På liknande sätt kan du implementera andra beteenden för ytterligare aspekter eller övergripande problem som du vill tillämpa på kommandon när du hanterar dem.
Ytterligare resurser
Medlarmönstret
-
Medlarmönster
https://en.wikipedia.org/wiki/Mediator_pattern
Dekoratörsmönstret
-
Mönster för dekoratör
https://en.wikipedia.org/wiki/Decorator_pattern
MediatR (Jimmy Bogard)
MediatR. GitHub-lagringsplats.
https://github.com/jbogard/MediatRCQRS med MediatR och AutoMapper
https://lostechies.com/jimmybogard/2015/05/05/cqrs-with-mediatr-and-automapper/Sätt kontrollanterna på en diet: POSTs och kommandon.
https://lostechies.com/jimmybogard/2013/12/19/put-your-controllers-on-a-diet-posts-and-commands/Ta itu med övergripande problem med en medlarpipeline
https://lostechies.com/jimmybogard/2014/09/09/tackling-cross-cutting-concerns-with-a-mediator-pipeline/CQRS och REST: den perfekta matchningen
https://lostechies.com/jimmybogard/2016/06/01/cqrs-and-rest-the-perfect-match/Exempel på MediatR-pipeline
https://lostechies.com/jimmybogard/2016/10/13/mediatr-pipeline-examples/Testarmaturer för lodrät sektor för MediatR och ASP.NET Core
https://lostechies.com/jimmybogard/2016/10/24/vertical-slice-test-fixtures-for-mediatr-and-asp-net-core/MediatR-tillägg för Microsoft Dependency Injection släppt
https://lostechies.com/jimmybogard/2016/07/19/mediatr-extensions-for-microsoft-dependency-injection-released/
Fluent-validering
- Jeremy Skinner. FluentValidation. GitHub-lagringsplats.
https://github.com/JeremySkinner/FluentValidation