Implementare le letture/query in un microservizio CQRS
Suggerimento
Questo contenuto è un estratto dell'eBook "Microservizi .NET: Architettura per le applicazioni .NET incluse in contenitori", disponibile in .NET Docs o come PDF scaricabile gratuitamente e da poter leggere offline.
Per le operazioni di lettura/query, il microservizio degli ordini dall'applicazione di riferimento eShopOnContainers implementa le query in modo indipendente dal modello DDD e dall'area transazionale. Questa implementazione è stata eseguita principalmente perché le richieste di query e per le transazioni sono drasticamente diverse. Le scritture eseguono le transazioni che devono essere conformi con la logica di dominio. Le query, d'altra parte, sono idempotenti e possono essere separate dalle regole di dominio.
L'approccio è semplice, come illustrato nella figura 7-3. L'interfaccia API viene implementata dai controller API Web usando qualsiasi infrastruttura, ad esempio un micro ORM (Object Relational Mapper) come Dapper, e restituendo ViewModel dinamici a seconda delle esigenze delle applicazioni dell'interfaccia utente.
Figura 7-3. L'approccio più semplice per le query in un microservizio CQRS
L'approccio più semplice per le query in un approccio CQRS semplificato può essere implementato eseguendo query sul database con un Micro-ORM come Dapper, restituendo viewModel dinamici. Le definizioni di query interrogano il database e restituiscono un ViewModel dinamico compilato in tempo reale per ogni query. Visto che le query sono idempotenti, non modificheranno i dati indipendentemente da quante volte si esegue una query. Di conseguenza, non si è necessariamente limitati da nessuno schema DDD usato nel lato transazionale, ad esempio aggregazioni e altri schemi, ed è per tale motivo che le query sono separate dall'area transazionale. È possibile eseguire una query sul database per i dati necessari all'interfaccia utente e restituire un viewModel dinamico che non deve essere definito in modo statico in qualsiasi punto (nessuna classe per ViewModels), tranne nelle istruzioni SQL stesse.
Visto che si tratta di un approccio semplice, il codice necessario per il lato di query (ad esempio il codice che usa un micro ORM come Dapper) può essere implementato nello stesso progetto API Web. La figura 7-4 illustra questo approccio. Le query sono definite nel progetto di microservizio Ordering.API all'interno della soluzione eShopOnContainers.
Figura 7-4. Query nel microservizio degli ordini in eShopOnContainers
Usare ViewModel creati in modo specifico per le applicazioni client, indipendentemente dai vincoli del modello di dominio
Visto che le query vengono eseguite per ottenere i dati necessari dalle applicazioni client, il tipo restituito può essere creato in modo specifico per i client, in base ai dati restituiti dalle query. Questi modelli, detti anche oggetti Data Transfer (DTO), vengono chiamati ViewModel.
I dati restituiti (ViewModel) possono essere il risultato dell'unione di dati da più entità o tabelle nel database o persino tra più aggregazioni definite nel modello di dominio per l'area transazionale. In questo caso, poiché si creano query indipendenti dal modello di dominio, i limiti e i vincoli delle aggregazioni vengono ignorati ed è possibile eseguire query su qualsiasi tabella e colonna necessaria. Questo approccio offre agli sviluppatori grande flessibilità e produttività per creare o aggiornare le query.
ViewModels può essere tipi statici definiti nelle classi, come implementato nel microservizio di ordinamento. Oppure possono essere creati in modo dinamico in base alle query eseguite, che è un procedimento agile per gli sviluppatori.
Usare Dapper come micro ORM per eseguire query
È possibile usare qualsiasi micro ORM, Entity Framework Core o persino il normale ADO.NET per l'esecuzione di query. Nell'applicazione di esempio, Dapper è stato selezionato per il microservizio degli ordini in eShopOnContainers come ottimo esempio di micro ORM più diffuso. Può eseguire query SQL semplici con prestazioni elevate, perché si tratta di un framework leggero. Con Dapper, è possibile scrivere una query SQL che può accedere e creare un join a più tabelle.
Dapper è un progetto open source (originalmente creato da Sam Saffron) e fa parte dei blocchi predefiniti usati nell'Overflow dello stack. Per usare Dapper, è sufficiente installarlo con il pacchetto NuGet Dapper, come illustrato nella figura riportata di seguito:
È anche necessario aggiungere una direttiva using
in modo che il codice abbia accesso ai metodi di estensione Dapper.
Quando si usa Dapper nel codice, si usa direttamente la classe SqlConnection disponibile nello spazio dei nomi Microsoft.Data.SqlClient. Tramite il metodo QueryAsync e altri metodi di estensione che estendono la classe SqlConnection, è possibile eseguire query in modo semplice ed efficiente.
ViewModel dinamici e statici a confronto
Quando i ViewModel vengono restituiti dal lato server alle app client, è possibile considerarli come DTO (Data Transfer Objects) che possono essere diversi per le entità del dominio interno del modello di entità, perché conservano i dati nel modo richiesto dall'app client. Di conseguenza, in molti casi, è possibile aggregare i dati provenienti da più entità di dominio e comporre il ViewModel con precisione in base al modo in cui l'applicazione client necessita di quei dati.
Tali oggetti ViewModel o DTO possono essere definiti in modo esplicito (come classi di titolari di dati), ad esempio la classe OrderSummary
illustrata in un frammento di codice successivo. In alternativa, è possibile restituire oggetti ViewModel dinamici o DTO dinamici in base agli attributi restituiti dalle query come tipo dinamico.
ViewModel come tipo dinamico
Come illustrato nel codice seguente, un ViewModel
può essere restituito direttamente dalle query con la semplice restituzione di un tipo dinamico internamente basato sugli attributi restituiti da una query. Ciò significa che il subset di attributi da restituire è basato sulla query stessa. Di conseguenza, se si aggiunge una nuova colonna alla query o al join, tali dati vengono aggiunti in modo dinamico al ViewModel
restituito.
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]");
}
}
}
L'aspetto importante è che, usando un tipo dinamico, la raccolta dei dati restituita viene assemblata in modo dinamico come ViewModel.
Vantaggi: questo approccio riduce la necessità di modificare le classi ViewModel statiche ogni volta che si aggiorna la frase SQL di una query, rendendo molto agile questo approccio di progettazione durante la codifica, con un'evoluzione semplice e rapida per quanto riguarda le modifiche future.
Svantaggi: a lungo termine, i tipi dinamici possono influire negativamente sulla chiarezza e sulla compatibilità di un servizio con le app client. In più, il software middleware come Swashbuckle non può fornire lo stesso livello di documentazione sui tipi restituiti se si usano tipi dinamici.
ViewModel come classi DTO predefinite
Vantaggi: avere classi ViewModel predefinite statiche, ad esempio "contratti" basati su classi DTO esplicite, è decisamente migliore per le API pubbliche, ma anche per i microservizi a lungo termine, anche se vengono usati solo dall'applicazione stessa.
Se si vogliono specificare i tipi di risposta per Swagger, è necessario usare le classi DTO esplicite come tipo restituito. Di conseguenza, le classi DTO predefinite consentono di offrire informazioni più complete da Swagger. In tal modo, la documentazione e la compatibilità delle API migliora durante il loro utilizzo.
Svantaggi: come indicato in precedenza, durante il suo aggiornamento, il codice richiede alcuni ulteriori passaggi per aggiornare le classi DTO.
Suggerimenti basati sulla nostra esperienza: nelle query implementate nel microservizio degli ordini in eShopOnContainers, abbiamo iniziato a sviluppare usando ViewModel dinamici perché era molto semplice e agile nelle prime fasi di sviluppo. Tuttavia, una volta che lo sviluppo si è stabilizzato, è stato scelto di eseguire il refactoring delle API e di usare DTO statici o predefiniti per i ViewModel, perché era più chiaro per i consumer del microservizio conoscere i tipi di DTO espliciti, usati come "contratti".
Nell'esempio seguente, è possibile visualizzare in che modo la query restituisce dati usando una classe DTO ViewModel esplicita: la classe 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]");
}
}
}
Descrivere i tipi di risposta delle API Web
Per gli sviluppatori che utilizzano API Web e microservizi è più importante ciò che viene restituito, in particolare i tipi di risposta e i codici di errore, se diversi da quelli standard. I tipi di risposta vengono gestiti nei commenti XML e nelle annotazioni di dati.
Senza la documentazione appropriata nell'interfaccia utente di Swagger, il consumer non riconosce i tipi restituiti né i codici HTTP che potrebbero esserlo. È possibile risolvere questo problema aggiungendo l'attributo Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute, in modo che Swashbuckle possa generare informazioni più complete sui modelli e i valori restituiti dall'API, come mostra il codice seguente:
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);
}
}
}
Tuttavia, l'attributo ProducesResponseType
non può usare un tipo dinamico, ma richiede l'uso di tipi espliciti, come il DTO ViewModel OrderSummary
, mostrato nell'esempio seguente:
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);
Si tratta di un altro motivo per cui i tipi restituiti espliciti sono migliori rispetto ai tipi dinamici, a lungo termine. Quando si usa l'attributo ProducesResponseType
, è anche possibile specificare il risultato previsto relativo a possibili errori/codici HTTP, ad esempio 200, 400 e così via.
Nella figura seguente, è possibile vedere in che modo l'interfaccia utente di Swagger mostra le informazioni ResponseType.
Figura 7-5. Interfaccia utente di Swagger che mostra i tipi di risposta e i possibili codici di stato HTTP da un'API Web
L'immagine mostra alcuni valori di esempio basati sui tipi ViewModel e sui possibili codici di stato HTTP che possono essere restituiti.
Risorse aggiuntive
Julie Lerman. Punti dati - Dapper, Entity Framework e App ibride (articolo msdn magazine)
https://video2.skills-academy.com/archive/msdn-magazine/2016/may/data-points-dapper-entity-framework-and-hybrid-appsPagine della Guida dell'API Web ASP.NET Core con Swagger
https://video2.skills-academy.com/aspnet/core/tutorials/web-api-help-pages-using-swagger?tabs=visual-studioCreare tipi di recordhttps://video2.skills-academy.com/dotnet/csharp/whats-new/tutorials/records