Поделиться через


Реализация операций чтения и запросов в микрослужбе CQRS

Совет

Это содержимое является фрагментом из электронной книги, архитектуры микрослужб .NET для контейнерных приложений .NET, доступных в документации .NET или в виде бесплатного скачиваемого PDF-файла, который можно читать в автономном режиме.

Архитектура микрослужб .NET для контейнерных приложений .NET для эскиза обложки.

При выполнении операций чтения и запросов микрослужба заказов в примере приложения eShopOnContainers реализует запросы независимо от модели проблемно-ориентированного программирования (DDD) и области транзакций. Это было сделано, главным образом, потому, что требования к запросам и транзакциям значительно отличаются. Операции записи выполняют транзакции, которые должны соответствовать логике предметной области. Запросы, с другой стороны, являются идемпотентными, их можно отделить от правил предметной области.

Это простой подход, как показано на рис. 7-3. API-интерфейс реализуется контроллерами веб-API с помощью любой инфраструктуры, например микро-ORM (средство объектно-реляционного отображения), такого как Dapper, и возвращения динамических ViewModel, в зависимости от потребностей приложения пользовательского интерфейса.

Схема, на которой в общем показана сторона запросов в упрощенной микрослужбе CQRS.

Рис. 7-3. Самый простой метод запросов в микрослужбе CQRS

Самый простой вариант для стороны запросов в упрощенном подходе CQRS можно реализовать путем запросов базы данных с помощью Micro-ORM, например Dapper, для возврата динамических ViewModel. Определения запроса обращаются к базе данных и возвращают динамическую ViewModel, которая создается в режиме реального времени для каждого запроса. Поскольку запросы идемпотентны, они не меняют данные, сколько бы раз вы ни выполняли запрос. Значит, вам не нужно ограничиваться шаблоном DDD, используемым на стороне транзакций, например статистическими выражениями и другими шаблонами. Поэтому запросы отделены от области транзакций. Вы запрашиваете у базы данных необходимые для пользовательского интерфейса данные и получаете динамические ViewModel, которые не нужно статически определять нигде, кроме самих инструкций SQL (для ViewModel нет классов).

Это простой подход, поэтому код на стороне запросов (например, код, использующий микро-ORM, вроде Dapper) можно реализовать в том же проекте веб-API. Это показано на рисунке 7-4. Запросы определяются в проекте микрослужбы Ordering.API в решении eShopOnContainers.

Снимок экрана: папка Queries в проекте Ordering.API.

Рис. 7-4. Запросы в микрослужбе заказов в eShopOnContainers

Использование моделей представления, специально созданных для клиентских приложений, независимо от ограничений модели предметной области

Поскольку запросы выполняются для получения данных, необходимых клиентским приложениям, можно создать тип возвращаемого значения специально для клиентов на основе данных, возвращаемых запросами. Эти модели, или объекты передачи данных (DTO), называются ViewModel.

Возвращаемые данные (ViewModel) могут быть результатом объединения данных из нескольких сущностей или таблиц в базе данных или даже нескольких статистических выражений, определенных в модели предметной области для области транзакций. В этом случае, так как вы создаете запросы независимо от модели предметной области, границы и ограничения статистических выражений игнорируются, и вы можете обратиться к любой нужной таблице или столбцу. Такой подход обеспечивает большую гибкость и производительность для разработчиков, создающих или обновляющих запросы.

ViewModels могут быть статическими типами, определенными в классах (как реализовано в микрослужбе заказов). Они также могут создаваться динамически в зависимости от выполненных запросов, что удобно для разработчиков.

Использование Dapper в качестве микро-ORM для выполнения запросов

Для запросов вы можете использовать любой микро-ORM, Entity Framework Core или даже просто ADO.NET. В примере приложения eShopOnContainers Dapper выбран для микрослужбы заказов как хороший пример популярного микро-ORM. Он может выполнять обычные SQL-запросы с высокой производительностью благодаря легкости платформы. С помощью Dapper вы можете написать SQL-запрос, который обратится к нескольким таблицам и соединит их.

Dapper — это проект с открытым кодом (изначально созданный Сэмом Сэффроном), который является частью стандартных блоков, используемых в Stack Overflow. Чтобы использовать Dapper, просто установите его с помощью пакета Dapper NuGet, как показано на рисунке:

Снимок экрана: пакет Dapper в представлении пакетов NuGet.

Затем добавьте директиву using, чтобы у кода был доступ к методам расширения Dapper.

При добавлении Dapper в код вы напрямую используете класс SqlConnection, доступный в пространстве имен Microsoft.Data.SqlClient. Метод QueryAsync и другие методы расширения, которые расширяют класс SqlConnection, — это прямой и эффективный способ выполнения запросов.

Динамические и статические модели ViewModel

Когда модели представления возвращаются с сервера в клиентские приложения, их можно представить себе в виде объектов передачи данных (DTO), которые могут отличаться для внутренних сущностей предметной области в вашей модели сущностей, поскольку модели представления хранят данные так, как это необходимо клиентскому приложению. Поэтому во многих случаях вы можете объединять данные из нескольких сущностей предметной области и составлять модели ViewModel в точном соответствии с требованиями клиентского приложения.

Эти ViewModel или DTO могут быть определены явным образом (в виде классов-владельцев данных), как и представленный выше класс OrderSummary. Вы также можете просто вернуть динамические ViewModel или DTO на основе атрибутов, полученных из запросов в виде динамических типов.

ViewModel как динамический тип

Как показано в следующем коде, ViewModel можно получить напрямую с помощью запросов, вернув динамический тип, который внутренне основан на атрибутах, возвращенных запросом. Это означает, что подмножество атрибутов, которое будет возвращено, основано на самом запросе. Поэтому, если вы добавляете в запрос или соединение новый столбец, данные динамически добавляются к возвращаемой 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]");
        }
    }
}

Важно отметить, что при использовании динамического типа возвращенная коллекция данных динамически собирается в виде ViewModel.

Преимущества. Этот подход снижает потребность в изменении статических классов ViewModel при обновлении предложения SQL запроса, что делает этот подход проектирования гибким при программировании, простом и быстром развитии в отношении будущих изменений.

Недостатки: в долгосрочной перспективе динамические типы могут негативно отразиться на ясности и повлиять на совместимость службы с клиентскими приложениями. Кроме того, ПО промежуточного слоя, например Swashbuckle, не может предоставить тот же уровень документации для типов возвращаемого значения, если вы используете динамические типы.

ViewModel как предопределенные классы объектов передачи данных

Преимущества. Наличие статических, предопределенных классов ViewModel, таких как "contracts" на основе явных классов DTO, определенно лучше подходит для общедоступных API, но и для долгосрочных микрослужб, даже если они используются только тем же приложением.

Если вы хотите указать типы ответов для Swagger, необходимо использовать явные классы объектов передачи данных в качестве типа возвращаемого значения. Таким образом, с помощью предопределенных классов объектов передачи данных вы сможете получить больше сведений из Swagger. Так вы сможете улучшить документацию по API и совместимость при использовании API.

Недостатки: как уже упоминалось, при обновлении кода будет сложнее обновить классы объектов передачи данных.

Совет на основе нашего опыта. В запросах, реализованных в микрослужбе заказа в eShopOnContainers, мы начали разработку с помощью динамических ViewModels, так как это было просто и гибко на ранних этапах разработки. Но после стабилизации разработки мы выполнили рефакторинг API и использовали статические или предопределенные объекты передачи данных для ViewModel, так как микрослужбам удобнее знать явные типы объектов передачи данных и использовать их как "контракты".

В следующем примере показано, как запрос возвращает данные с помощью явного класса объекта передачи данных ViewModel: класса 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]");
        }
    }
}

Описания типов ответов веб-API

Разработчиков, использующих веб-интерфейсов API и микрослужбы, в первую очередь волнуют возвращаемые данные, а именно: типы ответов и коды ошибок (если они не стандартны). Типы ответов обрабатываются в XML-комментариях и заметках к данным.

Без правильной документации в пользовательском интерфейсе Swagger потребителю не хватает знаний о том, какой тип значений возвращается или что могут вернуть HTTP-коды. Проблему можно решить, если добавить Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute, чтобы Swashbuckle создал больше информации о модели возврата и возвращенных значениях API, как в примере кода:

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

Однако атрибут ProducesResponseType не может использовать динамический тип. Он требует явный тип, как объект передачи данных ViewModel OrderSummary, показанный в следующем примере:

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

Это еще одна причина, по которой явные типы возвращаемого значения лучше динамических в долгосрочной перспективе. При использовании атрибута ProducesResponseType вы также можете указать ожидаемый результат в отношении возможных ошибок и кодов HTTP, например 200, 400 и т. д.

На следующем рисунке показано, как пользовательский интерфейс Swagger отображает сведения ResponseType.

Снимок экрана: страница пользовательского интерфейса Swagger для Ordering API.

Рис. 7-5. Пользовательский интерфейс Swagger, отображающий типы ответов и возможные коды состояния HTTP в веб-API

На рисунке выше показан пример значений на базе типов ViewModel и коды состояния HTTP, которые могут возвращаться.

Дополнительные ресурсы