Implementera läsningar/frågor i en CQRS-mikrotjänst
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.
För läsningar/frågor implementerar beställningsmikrotjänsten från referensprogrammet eShopOnContainers frågorna oberoende av DDD-modellen och transaktionsområdet. Den här implementeringen gjordes främst på grund av att kraven på frågor och transaktioner skiljer sig drastiskt. Skrivningar kör transaktioner som måste vara kompatibla med domänlogik. Frågor är å andra sidan idempotent och kan skiljas från domänreglerna.
Metoden är enkel, som du ser i bild 7–3. API-gränssnittet implementeras av webb-API-kontrollanterna med hjälp av valfri infrastruktur, till exempel en orm (Micro Object Relational Mapper) som Dapper, och returnerar dynamiska ViewModels beroende på UI-programmens behov.
Bild 7-3. Den enklaste metoden för frågor i en CQRS-mikrotjänst
Den enklaste metoden för frågesidan i en förenklad CQRS-metod kan implementeras genom att köra frågor mot databasen med en Micro-ORM som Dapper och returnera dynamiska ViewModels. Frågedefinitionerna frågar databasen och returnerar en dynamisk ViewModel som skapats i farten för varje fråga. Eftersom frågorna är idempotenter ändrar de inte data oavsett hur många gånger du kör en fråga. Därför behöver du inte begränsas av något DDD-mönster som används på transaktionssidan, till exempel aggregeringar och andra mönster, och det är därför frågor separeras från transaktionsområdet. Du frågar databasen efter de data som användargränssnittet behöver och returnerar en dynamisk ViewModel som inte behöver definieras statiskt någonstans (inga klasser för ViewModels) förutom i själva SQL-uttrycken.
Eftersom den här metoden är enkel kan koden som krävs för frågesidan (till exempel kod med hjälp av en mikro-ORM som Dapper) implementeras i samma webb-API-projekt. Bild 7–4 visar den här metoden. Frågorna definieras i mikrotjänstprojektet Ordering.API i lösningen eShopOnContainers.
Bild 7-4. Frågor i mikrotjänsten Ordering i eShopOnContainers
Använd ViewModels som är specifikt tillverkat för klientappar, oberoende av domänmodellbegränsningar
Eftersom frågorna utförs för att hämta de data som behövs av klientprogrammen kan den returnerade typen göras specifikt för klienterna, baserat på de data som returneras av frågorna. Dessa modeller eller DTU:er (Data Transfer Objects) kallas ViewModels.
Returnerade data (ViewModel) kan bero på att data kopplas från flera entiteter eller tabeller i databasen, eller till och med över flera aggregat som definierats i domänmodellen för transaktionsområdet. I det här fallet, eftersom du skapar frågor oberoende av domänmodellen, ignoreras aggregerade gränser och begränsningar och du kan köra frågor mot alla tabeller och kolumner som du kan behöva. Den här metoden ger stor flexibilitet och produktivitet för utvecklare som skapar eller uppdaterar frågorna.
ViewModels kan vara statiska typer som definieras i klasser (som implementeras i beställningsmikrotjänsten). Eller så kan de skapas dynamiskt baserat på de frågor som utförs, vilket är agilt för utvecklare.
Använda Dapper som en mikro-ORM för att utföra frågor
Du kan använda valfri mikro-ORM, Entity Framework Core eller till och med vanliga ADO.NET för frågor. I exempelprogrammet valdes Dapper för att beställa mikrotjänster i eShopOnContainers som ett bra exempel på en populär mikro-ORM. Den kan köra vanliga SQL-frågor med bra prestanda eftersom det är ett lätt ramverk. Med Dapper kan du skriva en SQL-fråga som kan komma åt och ansluta till flera tabeller.
Dapper är ett projekt med öppen källkod (ursprungligen skapat av Sam Saffron) och är en del av byggstenarna som används i Stack Overflow. Om du vill använda Dapper behöver du bara installera det via Dapper NuGet-paketet, som du ser i följande bild:
Du måste också lägga till ett using
direktiv så att koden har åtkomst till Dapper-tilläggsmetoderna.
När du använder Dapper i koden använder du direkt klassen som SqlConnection är tillgänglig i Microsoft.Data.SqlClient namnområdet. Med metoden QueryAsync och andra tilläggsmetoder som utökar SqlConnection klassen kan du köra frågor på ett enkelt och högpresterande sätt.
Dynamisk kontra statisk ViewModels
När du returnerar ViewModels från serversidan till klientappar kan du tänka på de ViewModels som DTU:er (dataöverföringsobjekt) som kan skilja sig från de interna domänentiteterna i din entitetsmodell eftersom ViewModels innehåller data som klientappen behöver. Därför kan du i många fall aggregera data som kommer från flera domänentiteter och skriva ViewModels exakt beroende på hur klientappen behöver dessa data.
Dessa ViewModels eller DTU:er kan definieras explicit (som datahållarklasser), som klassen OrderSummary
som visas i ett senare kodfragment. Eller så kan du bara returnera dynamiska ViewModels eller dynamiska DTU:er baserat på de attribut som returneras av dina frågor som en dynamisk typ.
ViewModel som dynamisk typ
Som visas i följande kod kan en ViewModel
returneras direkt av frågorna genom att bara returnera en dynamisk typ som internt baseras på de attribut som returneras av en fråga. Det innebär att delmängden av attribut som ska returneras baseras på själva frågan. Om du lägger till en ny kolumn i frågan eller kopplingen läggs därför dessa data dynamiskt till i den returnerade ViewModel
.
using Dapper;
using Microsoft.Extensions.Configuration;
using System.Data.SqlClient;
using System.Threading.Tasks;
using System.Dynamic;
using System.Collections.Generic;
public class OrderQueries : IOrderQueries
{
public async Task<IEnumerable<dynamic>> GetOrdersAsync()
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
return await connection.QueryAsync<dynamic>(
@"SELECT o.[Id] as ordernumber,
o.[OrderDate] as [date],os.[Name] as [status],
SUM(oi.units*oi.unitprice) as total
FROM [ordering].[Orders] o
LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid
LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id
GROUP BY o.[Id], o.[OrderDate], os.[Name]");
}
}
}
Det viktiga är att den returnerade datasamlingen med hjälp av en dynamisk typ monteras dynamiskt som ViewModel.
Fördelar: Den här metoden minskar behovet av att ändra statiska ViewModel-klasser när du uppdaterar SQL-meningen för en fråga, vilket gör den här designmetoden flexibel när du kodar, enkelt och snabbt utvecklas när det gäller framtida ändringar.
Nackdelar: På lång sikt kan dynamiska typer påverka tydligheten och kompatibiliteten för en tjänst med klientappar negativt. Dessutom kan mellanprogramsprogram som Swashbuckle inte tillhandahålla samma dokumentationsnivå för returnerade typer om du använder dynamiska typer.
ViewModel som fördefinierade DTO-klasser
Fördelar: Att ha statiska, fördefinierade ViewModel-klasser, som "kontrakt" baserade på explicita DTO-klasser, är definitivt bättre för offentliga API:er men även för långsiktiga mikrotjänster, även om de bara används av samma program.
Om du vill ange svarstyper för Swagger måste du använda explicita DTO-klasser som returtyp. Därför kan du med fördefinierade DTO-klasser erbjuda bättre information från Swagger. Det förbättrar API-dokumentationen och kompatibiliteten när du använder ett API.
Nackdelar: Som tidigare nämnts, när du uppdaterar koden, tar det några fler steg för att uppdatera DTO-klasserna.
Tips baserat på vår erfarenhet: I de frågor som implementerades vid ordermikrotjänsten i eShopOnContainers började vi utveckla med hjälp av dynamiska ViewModels eftersom det var enkelt och smidigt i de tidiga utvecklingsstegen. Men när utvecklingen har stabiliserats valde vi att omstrukturera API:erna och använda statiska eller fördefinierade DTU:er för ViewModels, eftersom det är tydligare för mikrotjänstens konsumenter att känna till explicita DTO-typer som används som "kontrakt".
I följande exempel kan du se hur frågan returnerar data med hjälp av en explicit ViewModel DTO-klass: klassen OrderSummary.
using Dapper;
using Microsoft.Extensions.Configuration;
using System.Data.SqlClient;
using System.Threading.Tasks;
using System.Dynamic;
using System.Collections.Generic;
public class OrderQueries : IOrderQueries
{
public async Task<IEnumerable<OrderSummary>> GetOrdersAsync()
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
return await connection.QueryAsync<OrderSummary>(
@"SELECT o.[Id] as ordernumber,
o.[OrderDate] as [date],os.[Name] as [status],
SUM(oi.units*oi.unitprice) as total
FROM [ordering].[Orders] o
LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid
LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id
GROUP BY o.[Id], o.[OrderDate], os.[Name]
ORDER BY o.[Id]");
}
}
}
Beskriva svarstyper för webb-API:er
Utvecklare som använder webb-API:er och mikrotjänster bryr sig mest om vad som returneras – särskilt svarstyper och felkoder (om inte standard). Svarstyperna hanteras i XML-kommentarer och dataanteckningar.
Utan korrekt dokumentation i Swagger-användargränssnittet saknar konsumenten kunskap om vilka typer som returneras eller vilka HTTP-koder som kan returneras. Det problemet åtgärdas genom att lägga till Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute, så att Swashbuckle kan generera mer detaljerad information om API-returmodellen och -värden, som du ser i följande kod:
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers
{
[Route("api/v1/[controller]")]
[Authorize]
public class OrdersController : Controller
{
//Additional code...
[Route("")]
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<OrderSummary>),
(int)HttpStatusCode.OK)]
public async Task<IActionResult> GetOrders()
{
var userid = _identityService.GetUserIdentity();
var orders = await _orderQueries
.GetOrdersFromUserAsync(Guid.Parse(userid));
return Ok(orders);
}
}
}
Attributet kan dock inte använda dynamiskt som en typ, ProducesResponseType
men kräver att explicita typer, till exempel OrderSummary
ViewModel DTO, används i följande exempel:
public class OrderSummary
{
public int ordernumber { get; set; }
public DateTime date { get; set; }
public string status { get; set; }
public double total { get; set; }
}
// or using C# 8 record types:
public record OrderSummary(int ordernumber, DateTime date, string status, double total);
Detta är en annan orsak till att explicita returnerade typer är bättre än dynamiska typer på lång sikt. När du använder ProducesResponseType
attributet kan du också ange vad som är det förväntade resultatet när det gäller möjliga HTTP-fel/-koder, t.ex. 200, 400 osv.
I följande bild kan du se hur Swagger-användargränssnittet visar ResponseType-informationen.
Bild 7-5. Swagger-användargränssnitt som visar svarstyper och möjliga HTTP-statuskoder från ett webb-API
Bilden visar några exempelvärden baserat på ViewModel-typerna och de möjliga HTTP-statuskoder som kan returneras.
Ytterligare resurser
Julie Lerman. Datapunkter – Dapper, Entity Framework och Hybrid Apps (artikel i MSDN-tidningen)
https://learn.microsoft.com/archive/msdn-magazine/2016/may/data-points-dapper-entity-framework-and-hybrid-appshjälpsidor för ASP.NET Core Web API med Swagger
https://learn.microsoft.com/aspnet/core/tutorials/web-api-help-pages-using-swagger?tabs=visual-studioSkapa posttyper https://learn.microsoft.com/dotnet/csharp/whats-new/tutorials/records