Compartilhar via


Implementando leituras/consultas em um microsserviço CQRS

Dica

Esse conteúdo é um trecho do eBook da Arquitetura de Microsserviços do .NET para os Aplicativos .NET em Contêineres, disponível no .NET Docs ou como um PDF para download gratuito que pode ser lido offline.

Imagem em miniatura da capa do eBook da Arquitetura de Microsserviços do .NET para os Aplicativos .NET em Contêineres.

Para leituras/consultas, o microsserviço de ordenação 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 exigências para consultas e transações são totalmente diferentes. Grava transações de execução que devem estar em conformidade com a lógica do domínio. Consultas, por outro lado, são idempotentes e podem ser separadas das regras de domínio.

A abordagem é simples, conforme mostra a Figura 7-3. A interface de API é implementada pelos controladores de API da Web usando qualquer infraestrutura, como um micro ORM (Mapeador Relacional de Objeto) como 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 de 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 dinamicamente para cada consulta. Uma vez que as consultas são idempotentes, elas não alteram os dados, não importa quantas vezes você execute uma consulta. Portanto, você não precisa estar restrito por nenhum padrão DDD usado no lado do transacional, como agregações e outros padrões, e é por isso que as consultas são separadas da área de trabalho transacional. Você consulta o banco de dados para os dados de que a interface do usuário precisa e retorna um ViewModel dinâmico que não precisa ser estaticamente definido em nenhum lugar (nenhuma classe para os ViewModels), exceto nas próprias instruções SQL.

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

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

Figura 7-4. Consultas no microsserviço de Ordenação em eShopOnContainers

Usar ViewModels feitos especificamente para aplicativos cliente, independentemente de restrições do modelo de domínio

Uma vez que as consultas são executadas 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 DTOs (Objetos de Transferência de Dados) são chamados de ViewModels.

Os dados retornados (ViewModel) podem ser o resultado da associação de dados de várias entidades ou tabelas no banco de dados, ou mesmo entre 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 de agregações e as restrições são ignorados e você fica livre para consultar qualquer tabela e coluna de que possa precisar. Essa abordagem fornece grande flexibilidade e produtividade para os desenvolvedores criarem ou atualizarem as consultas.

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

Usar 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 a consulta. O aplicativo de exemplo, o Dapper foi selecionado para o microsserviço de ordenação em eShopOnContainers como um bom exemplo de um micro ORM popular. Ele pode executar consultas SQL simples com alto desempenho, pois é uma estrutura leve. Usando o Dapper, você pode escrever uma consulta SQL que pode acessar e unir várias tabelas.

O Dapper é um projeto de software livre (original criado por Sam Saffron) e faz parte dos blocos de construção usado no Stack Overflow. Para usar o Dapper, basta instalá-lo por meio do pacote Dapper NuGet, conforme mostra a figura a seguir:

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

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

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

ViewModels dinâmicos versus estáticos

Ao retornar ViewModels do lado do servidor para aplicativos cliente, você pode pensar sobre esses ViewModels como DTOs (Objetos de Transferência de Dados) que podem ser diferentes para as entidades de domínio internas do seu modelo de entidade porque a os ViewModels contêm os dados da maneira como o aplicativo cliente precisa. Portanto, em muitos casos, você pode agregar dados provenientes de várias entidades de domínio e compor os ViewModels exatamente de acordo com a maneira como o aplicativo cliente precisa daqueles dados.

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

ViewModel como tipo dinâmico

Conforme mostrado no código a seguir, um ViewModel pode ser diretamente retornado pelas consultas retornando um tipo dinâmico 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 junção, esses dados serão adicionados dinamicamente ao ViewModel retornado.

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, ao usar um tipo dinâmico, a coleção de dados retornada é montada dinamicamente como o ViewModel.

Prós: essa abordagem reduz a necessidade de modificar classes estáticas de ViewModel sempre que você atualizar a frase SQL de uma consulta, tornando essa abordagem de design ágil ao codificar, simples e rápida de evoluir conforme alterações futuras.

Contras: no longo prazo, tipos dinâmicos podem afetar negativamente a clareza e a compatibilidade de um serviço com aplicativos cliente. Além disso, o software middleware como Swashbuckle não poderá fornecer o mesmo nível de documentação em tipos retornados ao usar tipos dinâmicos.

ViewModel como classes DTO predefinidas

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

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

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

Dica com base em nossa experiência: nas consultas implementadas no microsserviço de Ordenação em eShopOnContainers, começamos a desenvolver usando ViewModels dinâmico, pois ele era simples e mais ágil nos primeiros estágios de desenvolvimento. No entanto, quando o desenvolvimento foi estabilizado, escolhemos refatorar as APIs e usar DTOs estáticos ou predefinidos para ViewModels, pois fica mais claro para os consumidores do microsserviço conhecer os 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 os tipos de resposta de APIs da Web

Os desenvolvedores que consomem APIs e microsserviços Web 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 manipulados 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 Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute, portanto, o Swashbuckle pode gerar informações mais avançadas sobre o modelo e os valores de retorno de API, conforme mostra o 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 atributo ProducesResponseType não pode usar dinâmica 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);

Essa é outra razão pela qual tipos retornados explícitos são melhores que tipos dinâmicos no longo prazo. Ao usar o atributo ProducesResponseType, também é possível 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 Swagger mostra as informações de ResponseType.

Captura de tela da página da interface do usuário do Swagger para a API de ordenação.

Figura 7-5. Interface do usuário do Swagger mostrando os 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 de ViewModel e os possíveis códigos de status HTTP que podem ser retornados.

Recursos adicionais