Partilhar via


Implementar leituras/consultas em um microsserviço CQRS

Gorjeta

Este conteúdo é um trecho do eBook, .NET Microservices Architecture for Containerized .NET Applications, disponível no .NET Docs ou como um PDF para download gratuito que pode ser lido offline.

Miniatura da capa do eBook .NET Microservices Architecture for Containerized .NET Applications.

Para leituras/consultas, o microsserviço de pedidos do aplicativo de referência eShopOnContainers implementa as consultas independentemente do modelo DDD e da área transacional. Essa implementação foi feita principalmente porque as demandas por consultas e por transações são drasticamente diferentes. As gravações executam transações que devem ser compatíveis com a lógica do domínio. As consultas, por outro lado, são idempotentes e podem ser segregadas das regras de domínio.

A abordagem é simples, como mostra a Figura 7-3. A interface da API é implementada pelos controladores de API da Web usando qualquer infraestrutura, como um micro Object Relational Mapper (ORM) como o Dapper, e retornando ViewModels dinâmicos dependendo das necessidades dos aplicativos de interface do usuário.

Diagrama mostrando o lado das consultas de alto nível no CQRS simplificado.

Figura 7-3. A abordagem mais simples para consultas em um microsserviço CQRS

A abordagem mais simples para o lado das consultas em uma abordagem CQRS simplificada pode ser implementada consultando o banco de dados com um Micro-ORM como o Dapper, retornando ViewModels dinâmicos. As definições de consulta consultam o banco de dados e retornam um ViewModel dinâmico criado instantaneamente para cada consulta. Como as consultas são idempotentes, elas não alterarão os dados, não importa quantas vezes você execute uma consulta. Portanto, você não precisa ser restringido por nenhum padrão DDD usado no lado transacional, como agregados e outros padrões, e é por isso que as consultas são separadas da área transacional. Você consulta o banco de dados para os dados que a interface do usuário precisa e retorna um ViewModel dinâmico que não precisa ser definido estaticamente em qualquer lugar (sem classes para os ViewModels), exceto nas próprias instruções SQL.

Como essa abordagem é simples, o código necessário para o lado das consultas (como o código usando um micro ORM como o Dapper) pode ser implementado dentro do mesmo projeto de API da Web. A Figura 7-4 mostra esta abordagem. As consultas são definidas no projeto de microsserviço Ordering.API dentro da solução eShopOnContainers.

Captura de tela da pasta Consultas do projeto Ordering.API.

Figura 7-4. Consultas no microsserviço Encomendar em eShopOnContainers

Use ViewModels criados especificamente para aplicativos cliente, independentemente de restrições de modelo de domínio

Como as consultas são realizadas para obter os dados necessários para os aplicativos cliente, o tipo retornado pode ser feito especificamente para os clientes, com base nos dados retornados pelas consultas. Esses modelos, ou Data Transfer Objects (DTOs), são chamados ViewModels.

Os dados retornados (ViewModel) podem ser o resultado da junção de dados de várias entidades ou tabelas no banco de dados, ou até mesmo em várias agregações definidas no modelo de domínio para a área transacional. Nesse caso, como você está criando consultas independentes do modelo de domínio, os limites e restrições de agregação são ignorados e você é livre para consultar qualquer tabela e coluna que possa precisar. Essa abordagem oferece grande flexibilidade e produtividade para os desenvolvedores que criam ou atualizam as consultas.

Os ViewModels podem ser tipos estáticos definidos em classes (como é implementado no microsserviço de ordenação). Ou podem ser criados dinamicamente com base nas consultas realizadas, o que é ágil para os desenvolvedores.

Use o Dapper como um micro ORM para executar consultas

Você pode usar qualquer micro ORM, Entity Framework Core ou até mesmo ADO.NET simples para consultas. No aplicativo de exemplo, o Dapper foi selecionado para o microsserviço de pedidos no eShopOnContainers como um bom exemplo de um micro ORM popular. Ele pode executar consultas SQL simples com ótimo desempenho, porque é uma estrutura leve. Usando o Dapper, você pode escrever uma consulta SQL que pode acessar e unir várias tabelas.

Dapper é um projeto de código aberto (original criado por Sam Saffron), e faz parte dos blocos de construção usados no Stack Overflow. Para usar o Dapper, você só precisa instalá-lo através do pacote Dapper NuGet, como mostra a figura a seguir:

Captura de tela do pacote Dapper na visualização de pacotes NuGet.

Você também precisa adicionar uma using diretiva para que seu código tenha acesso aos métodos de extensão do Dapper.

Ao usar o Dapper em seu código, você usa diretamente a SqlConnection classe disponível no Microsoft.Data.SqlClient namespace. Por meio do método QueryAsync e outros métodos de extensão que estendem a SqlConnection classe, você pode executar consultas de forma simples e com desempenho.

ViewModels dinâmicos versus estáticos

Ao retornar ViewModels do lado do servidor para aplicativos cliente, você pode pensar nesses ViewModels como DTOs (Data Transfer Objects) que podem ser diferentes das entidades de domínio interno do seu modelo de entidade porque os ViewModels armazenam os dados da maneira que o aplicativo cliente precisa. Portanto, em muitos casos, você pode agregar dados provenientes de várias entidades de domínio e compor os ViewModels precisamente de acordo com como o aplicativo cliente precisa desses dados.

Esses ViewModels ou DTOs podem ser definidos explicitamente (como classes de titular de dados), como a classe mostrada OrderSummary em um trecho de código posterior. Ou, você pode simplesmente retornar ViewModels dinâmicos ou DTOs dinâmicos com base nos atributos retornados por suas consultas como um tipo dinâmico.

ViewModel como tipo dinâmico

Como mostrado no código a seguir, um ViewModel pode ser retornado diretamente pelas consultas apenas retornando um tipo dinâmico que internamente é baseado nos atributos retornados por uma consulta. Isso significa que o subconjunto de atributos a serem retornados é baseado na própria consulta. Portanto, se você adicionar uma nova coluna à consulta ou associação, esses dados serão adicionados dinamicamente ao .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]");
        }
    }
}

O ponto importante é que, usando um tipo dinâmico, a coleção de dados retornada é montada dinamicamente como ViewModel.

Prós: Essa abordagem reduz a necessidade de modificar classes ViewModel estáticas sempre que você atualiza a frase SQL de uma consulta, tornando essa abordagem de design ágil ao codificar, direta e rápida para evoluir em relação a alterações futuras.

Contras: A longo prazo, os tipos dinâmicos podem afetar negativamente a clareza e a compatibilidade de um serviço com aplicativos cliente. Além disso, o software de middleware como o Swashbuckle não pode fornecer o mesmo nível de documentação sobre os tipos retornados se estiver usando tipos dinâmicos.

ViewModel como classes DTO predefinidas

Prós: Ter classes ViewModel estáticas e predefinidas, como "contratos" baseados em classes DTO explícitas, é definitivamente melhor para APIs públicas, mas também para microsserviços de longo prazo, mesmo que eles sejam usados apenas pelo mesmo aplicativo.

Se quiser especificar tipos de resposta para Swagger, você precisará usar classes DTO explícitas como o tipo de retorno. Portanto, as classes DTO predefinidas permitem que você ofereça informações mais ricas do Swagger. Isso melhora a documentação e a compatibilidade da API ao consumir uma API.

Contras: Como mencionado anteriormente, ao atualizar o código, são necessárias mais algumas etapas para atualizar as classes DTO.

Dica baseada em nossa experiência: Nas consultas implementadas no microsserviço de pedidos no eShopOnContainers, começamos a desenvolver usando ViewModels dinâmicos, pois era simples e ágil nos estágios iniciais de desenvolvimento. Mas, uma vez estabilizado o desenvolvimento, optamos por refatorar as APIs e usar DTOs estáticos ou pré-definidos para os ViewModels, porque é mais claro para os consumidores do microsserviço conhecer tipos de DTO explícitos, usados como "contratos".

No exemplo a seguir, você pode ver como a consulta está retornando dados usando uma classe DTO ViewModel explícita: a 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]");
        }
    }
}

Descrever tipos de resposta de APIs da Web

Os desenvolvedores que consomem APIs da Web e microsserviços estão mais preocupados com o que é retornado — especificamente tipos de resposta e códigos de erro (se não padrão). Os tipos de resposta são tratados nos comentários XML e anotações de dados.

Sem a documentação adequada na interface do usuário do Swagger, o consumidor não tem conhecimento de quais tipos estão sendo retornados ou quais códigos HTTP podem ser retornados. Esse problema é corrigido adicionando o , para que o Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttributeSwashbuckle possa gerar informações mais ricas sobre o modelo e os valores de retorno da API, conforme mostrado no código a seguir:

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

No entanto, o ProducesResponseType atributo não pode usar dynamic como um tipo, mas requer o uso de tipos explícitos, como o OrderSummary ViewModel DTO, mostrado no exemplo a seguir:

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

Esta é outra razão pela qual os tipos retornados explícitos são melhores do que os tipos dinâmicos, a longo prazo. Ao usar o ProducesResponseType atributo, você também pode especificar qual é o resultado esperado em relação a possíveis erros/códigos HTTP, como 200, 400, etc.

Na imagem a seguir, você pode ver como a interface do usuário do Swagger mostra as informações do ResponseType.

Captura de tela da página da interface do usuário do Swagger para a API de pedidos.

Figura 7-5. Interface do usuário do Swagger mostrando tipos de resposta e possíveis códigos de status HTTP de uma API da Web

A imagem mostra alguns valores de exemplo com base nos tipos ViewModel e os possíveis códigos de status HTTP que podem ser retornados.

Recursos adicionais