Implementowanie odczytów/zapytań w mikrousłudze CQRS

Napiwek

Ta zawartość jest fragmentem książki eBook, architektury mikrousług platformy .NET dla konteneryzowanych aplikacji platformy .NET dostępnych na platformie .NET Docs lub jako bezpłatnego pliku PDF, który można odczytać w trybie offline.

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

W przypadku operacji odczytu/zapytań mikrousługa porządkowania z aplikacji referencyjnej eShopOnContainers implementuje zapytania niezależnie od modelu DDD i obszaru transakcyjnego. Ta implementacja została wykonana przede wszystkim dlatego, że wymagania dotyczące zapytań i transakcji są drastycznie różne. Zapisy wykonują transakcje, które muszą być zgodne z logiką domeny. Zapytania, z drugiej strony, są idempotentne i mogą być oddzielone od reguł domeny.

Podejście jest proste, jak pokazano na rysunku 7–3. Interfejs API jest implementowany przez kontrolery interfejsu API sieci Web przy użyciu dowolnej infrastruktury, takiej jak mikro object Relational Mapper (ORM), takich jak Dapper, i zwraca dynamiczne modele ViewModel w zależności od potrzeb aplikacji interfejsu użytkownika.

Diagram showing high-level queries-side in simplified CQRS.

Rysunek 7–3. Najprostsze podejście do zapytań w mikrousłudze CQRS

Najprostszym podejściem po stronie zapytań w uproszczonym podejściu CQRS można zaimplementować, wykonując zapytanie względem bazy danych za pomocą mikro-ORM, takiego jak Dapper, zwracając dynamiczne modele ViewModels. Definicje zapytań wysyłają zapytanie do bazy danych i zwracają dynamiczną model ViewModel utworzoną na bieżąco dla każdego zapytania. Ponieważ zapytania są idempotentne, nie zmieniają danych niezależnie od tego, ile razy uruchamiasz zapytanie. W związku z tym nie trzeba ograniczać żadnego wzorca DDD używanego po stronie transakcyjnej, takich jak agregacje i inne wzorce, i dlatego zapytania są oddzielone od obszaru transakcyjnego. Wysyłasz zapytanie do bazy danych dla danych, których interfejs użytkownika potrzebuje i zwraca dynamiczny model ViewModel, który nie musi być zdefiniowany statycznie w dowolnym miejscu (bez klas dla modelu ViewModels), z wyjątkiem samych instrukcji SQL.

Ponieważ takie podejście jest proste, kod wymagany dla strony zapytań (na przykład kod korzystający z mikrousług lub narzędzia Dapper) można zaimplementować w ramach tego samego projektu internetowego interfejsu API. Rysunek 7–4 przedstawia to podejście. Zapytania są definiowane w projekcie mikrousługi Ordering.API w rozwiązaniu eShopOnContainers.

Screenshot of the Ordering.API project's Queries folder.

Rysunek 7–4. Zapytania w mikrousłudze zamawiania w eShopOnContainers

Używanie modeli ViewModel przeznaczonych specjalnie dla aplikacji klienckich, niezależnie od ograniczeń modelu domeny

Ponieważ zapytania są wykonywane w celu uzyskania danych wymaganych przez aplikacje klienckie, zwracany typ może być specjalnie wykonany dla klientów na podstawie danych zwracanych przez zapytania. Te modele lub obiekty transferu danych (DTO) są nazywane Modelami widoków.

Zwrócone dane (ViewModel) mogą być wynikiem łączenia danych z wielu jednostek lub tabel w bazie danych, a nawet w wielu agregacjach zdefiniowanych w modelu domeny dla obszaru transakcyjnego. W takim przypadku, ponieważ tworzysz zapytania niezależne od modelu domeny, granice agregacji i ograniczenia są ignorowane i możesz wykonywać zapytania dotyczące dowolnej tabeli i kolumny, które mogą być potrzebne. Takie podejście zapewnia dużą elastyczność i produktywność deweloperów tworzących lub aktualizując zapytania.

Modele ViewModel mogą być typami statycznymi zdefiniowanymi w klasach (zgodnie z implementacją w mikrousłudze porządkowania). Można je również tworzyć dynamicznie na podstawie wykonywanych zapytań, co jest elastyczne dla deweloperów.

Używanie narzędzia Dapper jako mikro ORM do wykonywania zapytań

Do wykonywania zapytań można użyć dowolnego mikrousług, programu Entity Framework Core, a nawet zwykłego ADO.NET. W przykładowej aplikacji narzędzie Dapper zostało wybrane do zamawiania mikrousługi w eShopOnContainers jako dobry przykład popularnego mikrousługi ORM. Może ona uruchamiać zwykłe zapytania SQL o doskonałej wydajności, ponieważ jest to jasna struktura. Za pomocą języka Dapper możesz napisać zapytanie SQL, które może uzyskiwać dostęp do wielu tabel i łączyć je.

Dapper to projekt open source (oryginalny utworzony przez Sam Saffron) i jest częścią bloków konstrukcyjnych używanych w witrynie Stack Overflow. Aby użyć narzędzia Dapper, wystarczy zainstalować go za pośrednictwem pakietu NuGet języka Dapper, jak pokazano na poniższej ilustracji:

Screenshot of the Dapper package in the NuGet packages view.

Należy również dodać dyrektywę using , aby kod miał dostęp do metod rozszerzenia Dapper.

Jeśli używasz języka Dapper w kodzie, bezpośrednio użyjesz SqlConnection klasy dostępnej Microsoft.Data.SqlClient w przestrzeni nazw. Za pomocą metody QueryAsync i innych metod rozszerzeń, które rozszerzają SqlConnection klasę, można uruchamiać zapytania w prosty i wydajny sposób.

Modele dynamiczne i statyczne

Podczas zwracania modelu ViewModels po stronie serwera do aplikacji klienckich można traktować te modele ViewModel jako obiekty DTO (obiekty transferu danych), które mogą różnić się od wewnętrznych jednostek domeny modelu jednostki, ponieważ model ViewModels przechowuje dane tak, jak potrzebuje aplikacja kliencka. W związku z tym w wielu przypadkach można agregować dane pochodzące z wielu jednostek domeny i tworzyć modele ViewModel dokładnie zgodnie ze sposobem, w jaki aplikacja kliencka potrzebuje tych danych.

Te modele ViewModel lub DTO można jawnie zdefiniować (jako klasy posiadacza danych), podobnie jak OrderSummary klasa pokazana w późniejszym fragmencie kodu. Możesz też zwrócić dynamiczne modele ViewModel lub dynamiczne obiekty DTO na podstawie atrybutów zwracanych przez zapytania jako typ dynamiczny.

ViewModel jako typ dynamiczny

Jak pokazano w poniższym kodzie, ViewModel zapytania mogą być zwracane bezpośrednio przez zapytania, zwracając tylko typ dynamiczny , który wewnętrznie jest oparty na atrybutach zwracanych przez zapytanie. Oznacza to, że podzestaw atrybutów do zwrócenia jest oparty na samym zapytaniu. W związku z tym w przypadku dodania nowej kolumny do zapytania lub sprzężenia dane te są dynamicznie dodawane do zwracanego ViewModelelementu .

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

Ważnym punktem jest to, że przy użyciu typu dynamicznego zwracana kolekcja danych jest dynamicznie składana jako model ViewModel.

Zalety: takie podejście zmniejsza konieczność modyfikowania statycznych klas ViewModel za każdym razem, gdy aktualizujesz zdanie SQL zapytania, dzięki czemu to podejście projektowe będzie zwinne podczas kodowania, proste i szybkie, aby rozwijać się w odniesieniu do przyszłych zmian.

Wady: W dłuższej perspektywie typy dynamiczne mogą negatywnie wpływać na przejrzystość i zgodność usługi z aplikacjami klienckimi. Ponadto oprogramowanie pośredniczące, takie jak Swashbuckle, nie może zapewnić tego samego poziomu dokumentacji zwracanych typów w przypadku używania typów dynamicznych.

ViewModel jako wstępnie zdefiniowane klasy DTO

Zalety: Posiadanie statycznych, wstępnie zdefiniowanych klas ViewModel, takich jak "contracts" na podstawie jawnych klas DTO, jest zdecydowanie lepsze dla publicznych interfejsów API, ale także dla długoterminowych mikrousług, nawet jeśli są one używane tylko przez tę samą aplikację.

Jeśli chcesz określić typy odpowiedzi dla struktury Swagger, musisz użyć jawnych klas DTO jako typu zwracanego. W związku z tym wstępnie zdefiniowane klasy DTO umożliwiają oferowanie bogatszych informacji z programu Swagger. Poprawia to dokumentację interfejsu API i zgodność podczas korzystania z interfejsu API.

Wady: Jak wspomniano wcześniej, podczas aktualizowania kodu należy wykonać kilka dodatkowych kroków w celu zaktualizowania klas DTO.

Porada oparta na naszym doświadczeniu: W zapytaniach implementowanych w mikrousłudze Ordering w eShopOnContainers zaczęliśmy opracowywać przy użyciu dynamicznych modelu ViewModels, ponieważ było to proste i elastyczne na wczesnych etapach programowania. Jednak po ustabilizowaniu rozwoju wybraliśmy refaktoryzowanie interfejsów API i używanie statycznych lub wstępnie zdefiniowanych obiektów DTO dla modelu ViewModels, ponieważ jest jaśniejsze dla konsumentów mikrousługi, aby znać jawne typy DTO, używane jako "kontrakty".

W poniższym przykładzie widać, jak zapytanie zwraca dane przy użyciu jawnej klasy DTO ViewModel: klasy 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]");
        }
    }
}

Opis typów odpowiedzi internetowych interfejsów API

Deweloperzy korzystający z internetowych interfejsów API i mikrousług są najbardziej zainteresowani tym, co jest zwracane — w szczególności typy odpowiedzi i kody błędów (jeśli nie są standardowe). Typy odpowiedzi są obsługiwane w komentarzach XML i adnotacjach danych.

Bez odpowiedniej dokumentacji w interfejsie użytkownika struktury Swagger użytkownik nie ma wiedzy na temat zwracanych typów lub zwracanych kodów HTTP. Ten problem został rozwiązany przez dodanie elementu Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute, aby pakiet Swashbuckle mógł wygenerować bogatsze informacje o modelu i wartościach zwracanych przez interfejs API, jak pokazano w poniższym kodzie:

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

Jednak atrybut nie może używać dynamicznego jako typu, ProducesResponseType ale wymaga użycia jawnych typów, takich jak OrderSummary Obiekt DTO modelu ViewModel, pokazany w poniższym przykładzie:

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

Jest to kolejny powód, dla którego jawne zwracane typy są lepsze niż typy dynamiczne w dłuższej perspektywie. W przypadku używania atrybutu ProducesResponseType można również określić oczekiwany wynik dotyczący możliwych błędów/kodów HTTP, takich jak 200, 400 itp.

Na poniższej ilustracji widać, jak interfejs użytkownika struktury Swagger pokazuje informacje ResponseType.

Screenshot of the Swagger UI page for the Ordering API.

Rysunek 7–5. Interfejs użytkownika programu Swagger przedstawiający typy odpowiedzi i możliwe kody stanu HTTP z internetowego interfejsu API

Obraz przedstawia kilka przykładowych wartości na podstawie typów ViewModel i możliwych kodów stanu HTTP, które można zwrócić.

Dodatkowe zasoby