Delen via


Lees-/query's implementeren in een CQRS-microservice

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.

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

Voor lees-/query's implementeert de ordermicroservice uit de referentietoepassing eShopOnContainers de query's onafhankelijk van het DDD-model en het transactionele gebied. Deze implementatie is voornamelijk uitgevoerd omdat de eisen voor query's en voor transacties drastisch verschillen. Schrijfbewerkingen voeren transacties uit die compatibel moeten zijn met de domeinlogica. Query's zijn daarentegen idempotent en kunnen worden gescheiden van de domeinregels.

De benadering is eenvoudig, zoals wordt weergegeven in afbeelding 7-3. De API-interface wordt geïmplementeerd door de Web API-controllers met behulp van een infrastructuur, zoals een micro Object Relational Mapper (ORM) zoals Dapper, en het retourneren van dynamische ViewModels, afhankelijk van de behoeften van de UI-toepassingen.

Diagram met query's op hoog niveau in vereenvoudigde CQRS.

Afbeelding 7-3. De eenvoudigste benadering voor query's in een CQRS-microservice

De eenvoudigste benadering voor de query's in een vereenvoudigde CQRS-benadering kan worden geïmplementeerd door een query uit te voeren op de database met een Micro-ORM zoals Dapper, waarmee dynamische ViewModels worden geretourneerd. De querydefinities voeren een query uit op de database en retourneren een dynamisch ViewModel dat op elk gewenst moment is gebouwd voor elke query. Omdat de query's idempotent zijn, worden de gegevens niet gewijzigd, ongeacht hoe vaak u een query uitvoert. Daarom hoeft u niet te worden beperkt door een DDD-patroon dat wordt gebruikt in de transactionele zijde, zoals aggregaties en andere patronen, en daarom worden query's gescheiden van het transactionele gebied. U voert een query uit op de database voor de gegevens die de gebruikersinterface nodig heeft en retourneert een dynamisch ViewModel dat nergens statisch hoeft te worden gedefinieerd (geen klassen voor de ViewModels), behalve in de SQL-instructies zelf.

Omdat deze methode eenvoudig is, kan de code die nodig is voor de queryzijde (zoals code met behulp van een micro ORM zoals Dapper) binnen hetzelfde web-API-project worden geïmplementeerd. In afbeelding 7-4 ziet u deze benadering. De query's worden gedefinieerd in het microserviceproject Ordering.API in de eShopOnContainers-oplossing.

Schermopname van de map Query's van het project Ordering.API.

Afbeelding 7-4. Query's in de microservice Bestellen in eShopOnContainers

ViewModels gebruiken die specifiek zijn gemaakt voor client-apps, onafhankelijk van beperkingen van domeinmodellen

Omdat de query's worden uitgevoerd om de gegevens te verkrijgen die nodig zijn voor de clienttoepassingen, kan het geretourneerde type specifiek worden gemaakt voor de clients, op basis van de gegevens die door de query's worden geretourneerd. Deze modellen, of DTU's (Data Transfer Objects), worden ViewModels genoemd.

De geretourneerde gegevens (ViewModel) kunnen het resultaat zijn van het samenvoegen van gegevens uit meerdere entiteiten of tabellen in de database, of zelfs van meerdere aggregaties die zijn gedefinieerd in het domeinmodel voor het transactionele gebied. Omdat u in dit geval query's maakt onafhankelijk van het domeinmodel, worden de grenzen en beperkingen van aggregaties genegeerd en kunt u query's uitvoeren op tabellen en kolommen die u mogelijk nodig hebt. Deze aanpak biedt veel flexibiliteit en productiviteit voor ontwikkelaars die de query's maken of bijwerken.

De ViewModels kunnen statische typen zijn gedefinieerd in klassen (zoals geïmplementeerd in de order microservice). Of ze kunnen dynamisch worden gemaakt op basis van de uitgevoerde query's, wat flexibel is voor ontwikkelaars.

Dapper gebruiken als micro ORM om query's uit te voeren

U kunt elke micro ORM, Entity Framework Core of zelfs ADO.NET gebruiken om query's uit te voeren. In de voorbeeldtoepassing werd Dapper geselecteerd voor de bestellende microservice in eShopOnContainers als een goed voorbeeld van een populaire micro ORM. Er kunnen eenvoudige SQL-query's worden uitgevoerd met geweldige prestaties, omdat het een licht framework is. Met Dapper kunt u een SQL-query schrijven waarmee meerdere tabellen kunnen worden geopend en samengevoegd.

Dapper is een opensource-project (oorspronkelijk gemaakt door Sam Saffron) en maakt deel uit van de bouwstenen die worden gebruikt in Stack Overflow. Als u Dapper wilt gebruiken, hoeft u het alleen te installeren via het Dapper NuGet-pakket, zoals wordt weergegeven in de volgende afbeelding:

Schermopname van het Dapper-pakket in de Weergave NuGet-pakketten.

U moet ook een using richtlijn toevoegen, zodat uw code toegang heeft tot de Dapper-extensiemethoden.

Wanneer u Dapper in uw code gebruikt, gebruikt u rechtstreeks de SqlConnection klasse die beschikbaar is in de Microsoft.Data.SqlClient naamruimte. Via de methode QueryAsync en andere extensiemethoden waarmee de SqlConnection klasse wordt uitgebreid, kunt u query's op een eenvoudige en performante manier uitvoeren.

Dynamisch versus statisch ViewModels

Wanneer u ViewModels van de serverzijde naar client-apps retourneert, kunt u deze ViewModels beschouwen als DTU's (Data Transfer Objects) die kunnen verschillen van de interne domeinentiteiten van uw entiteitsmodel, omdat de ViewModels de gegevens bevatten zoals de client-app nodig heeft. Daarom kunt u in veel gevallen gegevens uit meerdere domeinentiteiten aggregeren en de ViewModels precies opstellen op basis van hoe de client-app die gegevens nodig heeft.

Deze ViewModels of DTU's kunnen expliciet worden gedefinieerd (als gegevenshouderklassen), zoals de OrderSummary klasse die wordt weergegeven in een later codefragment. U kunt ook gewoon dynamische ViewModels of dynamische DTU's retourneren op basis van de kenmerken die door uw query's worden geretourneerd als dynamisch type.

ViewModel als dynamisch type

Zoals wordt weergegeven in de volgende code, kan een ViewModel rechtstreeks worden geretourneerd door de query's door alleen een dynamisch type te retourneren dat intern is gebaseerd op de kenmerken die door een query worden geretourneerd. Dit betekent dat de subset van kenmerken die moeten worden geretourneerd, is gebaseerd op de query zelf. Als u daarom een nieuwe kolom toevoegt aan de query of join, worden die gegevens dynamisch toegevoegd aan de geretourneerde ViewModelkolom.

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

Het belangrijkste punt is dat met behulp van een dynamisch type de geretourneerde verzameling gegevens dynamisch wordt samengesteld als viewmodel.

Voordelen: Deze benadering vermindert de noodzaak om statische ViewModel-klassen te wijzigen wanneer u de SQL-zin van een query bijwerkt, waardoor deze ontwerpbenadering flexibel is bij het coderen, eenvoudig en snel ontwikkelen met betrekking tot toekomstige wijzigingen.

Nadelen: Op de lange termijn kunnen dynamische typen een negatieve invloed hebben op de duidelijkheid en de compatibiliteit van een service met client-apps. Daarnaast kan middlewaresoftware zoals Swashbuckle niet hetzelfde documentatieniveau bieden voor geretourneerde typen als dynamische typen worden gebruikt.

ViewModel als vooraf gedefinieerde DTO-klassen

Voordelen: Het gebruik van statische, vooraf gedefinieerde ViewModel-klassen, zoals 'contracten' op basis van expliciete DTO-klassen, is zeker beter voor openbare API's, maar ook voor microservices op lange termijn, zelfs als ze alleen door dezelfde toepassing worden gebruikt.

Als u antwoordtypen voor Swagger wilt opgeven, moet u expliciete DTO-klassen gebruiken als retourtype. Daarom kunt u met vooraf gedefinieerde DTO-klassen uitgebreidere informatie uit Swagger aanbieden. Dit verbetert de API-documentatie en -compatibiliteit bij het gebruik van een API.

Nadelen: Zoals eerder vermeld, duurt het bij het bijwerken van de code nog enkele stappen om de DTO-klassen bij te werken.

Tip op basis van onze ervaring: In de query's die zijn geïmplementeerd bij de Order microservice in eShopOnContainers, zijn we begonnen met het ontwikkelen met behulp van dynamische ViewModels, omdat het eenvoudig en flexibel was in de vroege ontwikkelingsfasen. Maar zodra de ontwikkeling was gestabiliseerd, hebben we ervoor gekozen om de API's te herstructureren en statische of vooraf gedefinieerde DTU's te gebruiken voor de ViewModels, omdat het duidelijker is voor de gebruikers van de microservice om expliciete DTO-typen te kennen, gebruikt als 'contracten'.

In het volgende voorbeeld ziet u hoe de query gegevens retourneert met behulp van een expliciete ViewModel DTO-klasse: de klasse 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]");
        }
    }
}

Antwoordtypen van web-API's beschrijven

Ontwikkelaars die web-API's en microservices gebruiken, houden zich het meest bezig met wat wordt geretourneerd, met name antwoordtypen en foutcodes (indien niet standaard). De antwoordtypen worden verwerkt in de XML-opmerkingen en gegevensaantekeningen.

Zonder de juiste documentatie in de Swagger-gebruikersinterface ontbreekt de consument aan de kennis van welke typen worden geretourneerd of welke HTTP-codes kunnen worden geretourneerd. Dit probleem is opgelost door het toevoegen van de Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute, zodat Swashbuckle uitgebreidere informatie kan genereren over het API-retourmodel en -waarden, zoals wordt weergegeven in de volgende code:

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

Het ProducesResponseType kenmerk kan echter niet dynamisch worden gebruikt als een type, maar vereist expliciete typen, zoals de OrderSummary ViewModel DTO, zoals in het volgende voorbeeld:

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

Dit is een andere reden waarom expliciete geretourneerde typen op lange termijn beter zijn dan dynamische typen. Wanneer u het ProducesResponseType kenmerk gebruikt, kunt u ook opgeven wat het verwachte resultaat is met betrekking tot mogelijke HTTP-fouten/codes, zoals 200, 400, enzovoort.

In de volgende afbeelding ziet u hoe de Swagger-gebruikersinterface de ResponseType-informatie weergeeft.

Schermopname van de pagina Swagger UI voor de Order-API.

Afbeelding 7-5. Swagger-gebruikersinterface met antwoordtypen en mogelijke HTTP-statuscodes van een web-API

In de afbeelding ziet u enkele voorbeeldwaarden op basis van de ViewModel-typen en de mogelijke HTTP-statuscodes die kunnen worden geretourneerd.

Aanvullende bronnen