简单的CQRS实现与原始SQL和DDD

本文介绍了如何使用.NetCore快速构建一个简单的CQRS(命令查询职责分离)和DDD(领域驱动设计)实现的RESTAPI应用。文章通过示例代码展示了读模型通过原始SQL查询实现,而写模型采用DDD方法,强调了这种简单实现不需要最终一致性。此外,还涵盖了领域模型验证、领域事件的发布和处理等关键点。
摘要由CSDN通过智能技术生成

简单的CQRS实现与原始SQL和DDD

翻译原文地址

系列文章目录

一、简单的CQRS实现与原始SQL和DDD
二、使用EF的领域模型的封装和持久化透明(PI)
三、REST API数据验证
四、领域模型验证
五、如何发布和处理领域事件
六、处理领域事件:缺失的部分
七、发件箱模式(The Outbox Pattern)
八、.NET Core中的旁路缓存模式(Cache-Aside Pattern)

引言

我经常遇到关于CQRS模式实现的问题。甚至比我经常看到的"是在上下文中用ORM还是纯SQL访问数据库哪个更快”的讨论遇到的更多。

在这篇文章中,我想向你展示如何使用. Net Core快速实现简单的REST API应用程序。

开宗明义,这是CQRS最简单的版本——通过写模型(Write Model)的更新立即更新了读模型(Read Model),因此我们在这里不保证最终的一致性( eventual consistency)。然而,许多应用程序不需要最终的一致性,因为推荐使用两个单独的模型对读写进行逻辑划分,这种做法在大多数解决方案中更加有效。

特别地,对于这篇文章,我准备了例子,一个完全可用的应用程序,请参阅Github上的完整源代码:https://github.com/kgrzybek/sample-dotnet-core-cqrs-api

我的目标

下面是我想通过创建这个解决方案来实现的目标:

  1. 写模型和读模型的清晰分离和隔离。
  2. 使用读模型检索数据应该尽可能快。
  3. 写模型应该用DDD方法实现。DDD实现的级别应该取决于领域复杂性的级别。
  4. 应用程序逻辑应该与图形用户界面(GUI)分离。
  5. 所选的库应该是成熟的、知名的和受支持的。

设计

组件之间的高级流程如下:

image

正如你所看到的,读取的过程非常简单,因为我们应该尽可能快地查询数据。这里我们不需要更多的抽象层和复杂的方法。从查询对象(query object)获取参数,对数据库执行原始SQL并返回数据——仅此而已。

在写支持的情况下是不同的。写作通常需要更高级的技术,因为我们需要执行一些逻辑,做一些计算或简单地检查一些条件(特别是不变量[invariants])。通过ORM工具的变更跟踪(change tracking)和使用存储库模式(Repository Pattern),我们可以让我们的领域模型保持完整(好吧,几乎)。

解决方案

下图展示了用于完成读请求操作的组件之间的流程:

image

GUI负责创建Query对象:


/// <summary>
/// Get customer order details.
/// 获取订单详情
/// </summary>
/// <param name="orderId">Order ID.</param>
[Route("{customerId}/orders/{orderId}")]
[HttpGet]
[ProducesResponseType(typeof(OrderDetailsDto), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetCustomerOrderDetails([FromRoute]Guid orderId)
{
    var orderDetails = await _mediator.Send(new GetCustomerOrderDetailsQuery(orderId));
    return Ok(orderDetails);
}


internal class GetCustomerOrderDetailsQuery : IRequest<OrderDetailsDto>
{
    public Guid OrderId { get; }

    public GetCustomerOrderDetailsQuery(Guid orderId)
    {
        this.OrderId = orderId;
    }
}

然后,查询处理程序处理查询:


internal class GetCustomerOrderDetialsQueryHandler : IRequestHandler<GetCustomerOrderDetailsQuery, OrderDetailsDto>
{
    private readonly ISqlConnectionFactory _sqlConnectionFactory;

    public GetCustomerOrderDetialsQueryHandler(ISqlConnectionFactory sqlConnectionFactory)
    {
        this._sqlConnectionFactory = sqlConnectionFactory;
    }

    public async Task<OrderDetailsDto> Handle(GetCustomerOrderDetailsQuery request, CancellationToken cancellationToken)
    {
        using (var connection = this._sqlConnectionFactory.GetOpenConnection())
        {
            const string sql = "SELECT " +
                               "[Order].[Id], " +
                               "[Order].[IsRemoved], " +
                               "[Order].[Value] " +
                               "FROM orders.v_Orders AS [Order] " +
                               "WHERE [Order].Id = @OrderId";
            var order = await connection.QuerySingleOrDefaultAsync<OrderDetailsDto>(sql, new {request.OrderId});

            const string sqlProducts = "SELECT " +
                               "[Order].[ProductId] AS [Id], " +
                               "[Order].[Quantity], " +
                               "[Order].[Name] " +
                               "FROM orders.v_OrderProducts AS [Order] " +
                               "WHERE [Order].OrderId = @OrderId";
            var products = await connection.QueryAsync<ProductDto>(sqlProducts, new { request.OrderId });

            order.Products = products.AsList();

            return order;
        }
    }
}


public class SqlConnectionFactory : ISqlConnectionFactory, IDisposable
{
    private readonly string _connectionString;
    private IDbConnection _connection;

    public SqlConnectionFactory(string connectionString)
    {
        this._connectionString = connectionString;
    }

    public IDbConnection GetOpenConnection()
    {
        if (this._connection == null || this._connection.State != ConnectionState.Open)
        {
            this._connection = new SqlConnection(_connectionString);
            this._connection.Open();
        }

        return this._connection;
    }

    public void Dispose()
    {
        if (this._connection != null && this._connection.State == ConnectionState.Open)
        {
            this._connection.Dispose();
        }
    }
}

第一件事是获得打开的数据库连接,它是使用SqlConnectionFactory类实现的。这个类由IoC容器解析,具有HTTP请求生命周期范围,因此我们可以确定在请求处理期间只使用一个数据库连接。

第二件事是针对数据库准备和执行原始SQL。我尽量不直接引用表,而是引用数据库视图。这是一种创建抽象并将应用程序与数据库模式解耦的好方法,因为我希望尽可能地隐藏数据库内部。

对于SQL执行,我使用微型ORM Dapper库,因为它几乎和本地ADO.NET一样快,并且没有样板API(boilerplate API)。

写模型

下图展示了写请求操作的流程:

image

写请求处理开始类似于读,但我们创建的是命令对象而不是查询对象:


/// <summary>
/// Add customer order.
/// </summary>
/// <param name="customerId">Customer ID.</param>
/// <param name="request">Products list.</param>
[Route("{customerId}/orders")]
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.Created)]
public async Task<IActionResult> AddCustomerOrder(
    [FromRoute]Guid customerId, 
    [FromBody]CustomerOrderRequest request)
{
   await _mediator.Send(new AddCustomerOrderCommand(customerId, request.Products));
   return Created(string.Empty, null);
}

然后,CommandHandler被调用:


public class AddCustomerOrderCommandHandler : IRequestHandler<AddCustomerOrderCommand>
{
    private readonly ICustomerRepository _customerRepository;
    private readonly IProductRepository _productRepository;

    public AddCustomerOrderCommandHandler(
        ICustomerRepository customerRepository, 
        IProductRepository productRepository)
    {
        this._customerRepository = customerRepository;
        this._productRepository = productRepository;
    }

    public async Task<Unit> Handle(AddCustomerOrderCommand request, CancellationToken cancellationToken)
    {
        var customer = await this._customerRepository.GetByIdAsync(request.CustomerId);

        var selectedProducts = request.Products.Select(x => new OrderProduct(x.Id, x.Quantity)).ToList();
        var allProducts = await this._productRepository.GetAllAsync();

        var order = new Order(selectedProducts, allProducts);
        
        customer.AddOrder(order);

        await this._customerRepository.UnitOfWork.CommitAsync(cancellationToken);

        return Unit.Value;
    }
}

命令处理程序与查询处理程序看起来不一样。在这里,我们使用DDD方法对聚合(Aggregates)和实体(Entities)使用更高级别的抽象。我们需要它们,因为在这种情况下,要解决的问题往往比通常的读模式更复杂。命令处理程序将聚合水化(hydrates aggregate),调用聚合方法并将更改保存到数据库。

Customer 聚合可定义如下:

public class Customer : Entity
{
    public Guid Id { get; private set; }

    private readonly List<Order> _orders;

    private Customer()
    {
        this._orders = new List<Order>();
    }

    public void AddOrder(Order order)
    {
        this._orders.Add(order);

        this.AddDomainEvent(new OrderAddedEvent(order));
    }

    public void ChangeOrder(Guid orderId, List<OrderProduct> products, IReadOnlyCollection<Product> allProducts)
    {
        var order = this._orders.Single(x => x.Id == orderId);
        order.Change(products, allProducts);

        this.AddDomainEvent(new OrderChangedEvent(order));
    }

    public void RemoveOrder(Guid orderId)
    {
        var order = this._orders.Single(x => x.Id == orderId);
        order.Remove();

        this.AddDomainEvent(new OrderRemovedEvent(order));
    }
}

public class Order : Entity
{
    public Guid Id { get; private set; }
    private bool _isRemoved;
    private decimal _value;
    private List<OrderProduct> _orderProducts;

    private Order()
    {
        this._orderProducts = new List<OrderProduct>();
        this._isRemoved = false;
    }

    public Order(List<OrderProduct> orderProducts, IReadOnlyCollection<Product> allProducts)
    {
        this.Id = Guid.NewGuid();
        this._orderProducts = orderProducts;

        this.CalculateOrderValue(allProducts);
    }

    internal void Change(List<OrderProduct> products, IReadOnlyCollection<Product> allProducts)
    {
        foreach (var product in products)
        {
            var orderProduct = this._orderProducts.SingleOrDefault(x => x.ProductId == product.ProductId);
            if (orderProduct != null)
            {
                orderProduct.ChangeQuantity(product.Quantity);
            }
            else
            {
                this._orderProducts.Add(product);
            }
        }

        var existingProducts = this._orderProducts.ToList();
        foreach (var existingProduct in existingProducts)
        {
            var product = products.SingleOrDefault(x => x.ProductId == existingProduct.ProductId);
            if (product == null)
            {
                this._orderProducts.Remove(existingProduct);
            }
        }

        this.CalculateOrderValue(allProducts);
    }

    internal void Remove()
    {
        this._isRemoved = true;
    }

    private void CalculateOrderValue(IReadOnlyCollection<Product> allProducts)
    {
        this._value = this._orderProducts.Sum(x => x.Quantity * allProducts.Single(y => y.Id == x.ProductId).Price);
    }
}

架构

基于知名的洋葱架构(Onion Architecture)解决方案结构如下:

image

只定义了3个项目:

  • API项目,API端点和应用程序逻辑(命令和查询处理程序)使用功能文件夹(Feature Folders)方法。
  • 使用领域模型(Domain Model)的领域项目
  • 基础设施项目-集成了数据库。

image

总结

在这篇文章中,我试图提出最简单的方法来实现CQRS模式,使用原始sql脚本作为读模型端处理,DDD方法作为写模型端实现。这样做,我们能够在不损失开发速度的情况下实现更多的分离关注点(separation of concerns)。引入这种解决方案的成本非常低,而且它的回报非常快。

我没有详细描述DDD实现,所以我鼓励您再次检查示例应用程序的存储库(repository)——可以用作您的应用程序的工具包启动器,就像我的应用程序一样。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值