Implementace čtení a dotazů v mikroslužbě CQRS
Tip
Tento obsah je výňatek z eBooku, architektury mikroslužeb .NET pro kontejnerizované aplikace .NET, které jsou k dispozici na .NET Docs nebo jako zdarma ke stažení PDF, které lze číst offline.
Pro čtení a dotazy implementuje objednávky mikroslužby z referenční aplikace eShopOnContainers dotazy nezávisle na modelu DDD a transakční oblasti. Tato implementace byla provedena především proto, že požadavky na dotazy a transakce jsou výrazně odlišné. Zápisy provádějí transakce, které musí být v souladu s logikou domény. Dotazy jsou naopak idempotentní a dají se oddělit od pravidel domény.
Přístup je jednoduchý, jak je znázorněno na obrázku 7–3. Rozhraní API je implementováno řadiči webového rozhraní API pomocí jakékoli infrastruktury, jako je mikro Object Relational Mapper (ORM), jako je Dapper, a v závislosti na potřebách aplikací uživatelského rozhraní vrací dynamické modely ViewModel.
Obrázek 7–3 Nejjednodušší přístup k dotazům v mikroslužbě CQRS
Nejjednodušší přístup pro dotazy na straně zjednodušeného přístupu CQRS je možné implementovat dotazováním databáze pomocí Mikro-ORM, jako je Dapper, vracející dynamické modely ViewModel. Definice dotazů dotazují databázi a vrací dynamickou model ViewModel sestavený průběžně pro každý dotaz. Vzhledem k tomu, že dotazy jsou idempotentní, nebudou měnit data bez ohledu na to, kolikrát dotaz spustíte. Proto není nutné omezit žádný model DDD používaný na transakční straně, jako jsou agregace a jiné vzory, a proto jsou dotazy oddělené od transakční oblasti. Dotazujete databázi na data, která uživatelské rozhraní potřebuje, a vrátíte dynamický model ViewModel, který nemusí být staticky definován kdekoli (žádné třídy pro ViewModels) s výjimkou samotných příkazů SQL.
Vzhledem k tomu, že tento přístup je jednoduchý, je možné kód potřebný pro dotazy (například kód pomocí mikro ORM, jako je Dapper), implementovat v rámci stejného projektu webového rozhraní API. Tento přístup ukazuje obrázek 7–4. Dotazy jsou definovány v projektu mikroslužby Ordering.API v rámci řešení eShopOnContainers.
Obrázek 7–4 Dotazy v mikroslužbě Objednávání v eShopOnContainers
Použití modelů ViewModels speciálně vytvořených pro klientské aplikace nezávisle na omezeních doménového modelu
Vzhledem k tomu, že dotazy se provádějí za účelem získání dat potřebných klientskými aplikacemi, je možné vrácený typ vytvořit speciálně pro klienty na základě dat vrácených dotazy. Tyto modely neboli objekty pro přenos dat (DTO) se nazývají ViewModels.
Vrácená data (ViewModel) mohou být výsledkem spojení dat z více entit nebo tabulek v databázi nebo dokonce napříč více agregacemi definovanými v doménovém modelu pro transakční oblast. V tomto případě, protože vytváříte dotazy nezávislé na doménovém modelu, agregace hranic a omezení se ignorují a můžete se na libovolnou tabulku a sloupec, které budete potřebovat, dotazovat. Tento přístup poskytuje vývojářům vytváření nebo aktualizaci dotazů velkou flexibilitu a produktivitu.
Modely ViewModel mohou být statické typy definované ve třídách (jak je implementováno v mikroslužbě řazení). Nebo je můžete vytvářet dynamicky na základě provedených dotazů, což je agilní pro vývojáře.
Použití Dapperu jako mikro ORM k provádění dotazů
K dotazování můžete použít libovolnou mikro ORM, Entity Framework Core nebo dokonce prostý ADO.NET. V ukázkové aplikaci byl Dapper vybrán pro objednávku mikroslužby v eShopOnContainers jako dobrý příklad oblíbené mikro ORM. Může spouštět prosté dotazy SQL s velkým výkonem, protože se jedná o lehkou architekturu. Pomocí Dapperu můžete napsat dotaz SQL, který má přístup k více tabulkám a spojit je.
Dapper je opensourcový projekt (původní projekt vytvořený Samem Saffronem) a je součástí stavebních bloků používaných v Stack Overflow. Pokud chcete použít Dapper, stačí ho nainstalovat prostřednictvím balíčku Dapper NuGet, jak je znázorněno na následujícím obrázku:
Musíte také přidat direktivu using
, aby váš kód získal přístup k metodám rozšíření Dapper.
Když v kódu použijete Dapper, použijete přímo třídu dostupnou SqlConnection Microsoft.Data.SqlClient v oboru názvů. Prostřednictvím metody QueryAsync a dalších rozšiřujících metod, které rozšiřují SqlConnection třídu, můžete spouštět dotazy jednoduchým a výkonným způsobem.
Dynamické a statické modely ViewModels
Když vracíte modely ViewModel ze strany serveru do klientských aplikací, můžete si tyto modely ViewModel představit jako objekty DTO (Objekty přenosu dat), které se můžou lišit od interních entit domény modelu entity, protože modely ViewModels uchovávají data způsobem, který klientská aplikace potřebuje. V mnoha případech tedy můžete agregovat data pocházející z více entit domény a vytvářet modely ViewModel přesně podle toho, jak klientská aplikace tato data potřebuje.
Tyto modely ViewModel nebo DTOs lze definovat explicitně (jako třídy držitelů dat), jako je OrderSummary
třída zobrazená v pozdějším fragmentu kódu. Nebo můžete jednoduše vrátit dynamické modely ViewModel nebo dynamické objekty DTO na základě atributů vrácených vašimi dotazy jako dynamického typu.
Model ViewModel jako dynamický typ
Jak je znázorněno v následujícím kódu, ViewModel
dotazy mohou být přímo vráceny pouze dynamickým typem, který interně vychází z atributů vrácených dotazem. To znamená, že podmnožina atributů, které se mají vrátit, je založena na samotném dotazu. Proto pokud do dotazu nebo spojení přidáte nový sloupec, tato data se dynamicky přidají do vrácených ViewModel
dat .
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]");
}
}
}
Důležitým bodem je, že pomocí dynamického typu se vrácená kolekce dat dynamicky sestaví jako Model ViewModel.
Klady: Tento přístup snižuje potřebu upravovat statické třídy ViewModel při každé aktualizaci věty SQL dotazu, takže tento přístup návrhu je flexibilní při psaní kódu, jednoduchém a rychlém vývoji v souvislosti s budoucími změnami.
Nevýhody: V dlouhodobém horizontu můžou dynamické typy negativně ovlivnit srozumitelnost a kompatibilitu služby s klientskými aplikacemi. Kromě toho middleware software, jako je Swashbuckle, nemůže poskytnout stejnou úroveň dokumentace o vrácených typech, pokud používáte dynamické typy.
ViewModel jako předdefinované třídy DTO
Výhody: Statické, předdefinované třídy ViewModel, jako jsou "kontrakty" založené na explicitních třídách DTO, je rozhodně lepší pro veřejná rozhraní API, ale také pro dlouhodobé mikroslužby, i když jsou používány pouze stejnou aplikací.
Pokud chcete zadat typy odpovědí pro Swagger, musíte jako návratový typ použít explicitní třídy DTO. Proto předdefinované třídy DTO umožňují nabízet bohatší informace z Swaggeru. Tím se zlepší dokumentace k rozhraní API a kompatibilita při využívání rozhraní API.
Nevýhody: Jak už bylo zmíněno dříve, při aktualizaci kódu trvá několik dalších kroků k aktualizaci tříd DTO.
Tip založený na našich zkušenostech: V dotazech implementovaných v mikroslužbě Objednávání v eShopOnContainers jsme začali vyvíjet pomocí dynamických modelů ViewModel, protože to bylo jednoduché a agilní v počátečních fázích vývoje. Po stabilizaci vývoje jsme se ale rozhodli refaktorovat rozhraní API a pro modely ViewModels používat statické nebo předdefinované objekty DTO, protože pro uživatele mikroslužeb je jasnější znát explicitní typy DTO, které se používají jako "kontrakty".
V následujícím příkladu můžete vidět, jak dotaz vrací data pomocí explicitní třídy ViewModel DTO: OrderSummary třída.
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]");
}
}
}
Popis typů odpovědí webových rozhraní API
Vývojáři, kteří používají webová rozhraní API a mikroslužby, se nejvíce zajímají o to, co se vrací – konkrétně typy odpovědí a kódy chyb (pokud nejsou standardní). Typy odpovědí se zpracovávají v komentářích XML a datových poznámkách.
Bez správné dokumentace v uživatelském rozhraní Swaggeru spotřebitel nemá znalosti o tom, jaké typy se vrací nebo jaké kódy HTTP je možné vrátit. Tento problém je opraven přidáním Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute, takže Swashbuckle může generovat bohatší informace o návratovém modelu a hodnotách rozhraní API, jak je znázorněno v následujícím kódu:
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);
}
}
}
Atribut však nemůže použít dynamickou hodnotu jako typ, ProducesResponseType
ale vyžaduje použití explicitních typů, jako je OrderSummary
DTO modelu ViewModel, jak je znázorněno v následujícím příkladu:
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);
To je další důvod, proč jsou explicitní vrácené typy v dlouhodobém horizontu lepší než dynamické typy. Při použití atributu ProducesResponseType
můžete také určit, jaký je očekávaný výsledek týkající se možných chyb nebo kódů HTTP, například 200, 400 atd.
Na následujícím obrázku vidíte, jak Swagger UI zobrazuje informace o typu ResponseType.
Obrázek 7–5 Swagger UI zobrazující typy odpovědí a možné stavové kódy HTTP z webového rozhraní API
Obrázek ukazuje některé ukázkové hodnoty založené na typech ViewModel a možných stavových kódech HTTP, které je možné vrátit.
Další materiály
Julie Lermanová. Datové body – Dapper, Entity Framework a Hybrid Apps (článek o časopisu MSDN)
https://learn.microsoft.com/archive/msdn-magazine/2016/may/data-points-dapper-entity-framework-and-hybrid-appsStránky nápovědy k webovému rozhraní API technologie ASP.NET Core využívající Swagger
https://learn.microsoft.com/aspnet/core/tutorials/web-api-help-pages-using-swagger?tabs=visual-studioVytvoření typů záznamů https://learn.microsoft.com/dotnet/csharp/whats-new/tutorials/records