从命令运行查询违反命令查询分离?

Nig*_*888 6 asp.net-mvc design-patterns entity-framework command-query-separation

给定一个真实的匿名购物车,"AddToCart"工作流程必须执行以下步骤:

  1. 从数据库中查找当前产品.从产品中获取价格或使用服务来计算用户选择和其他产品属性的价格.(查询)
  2. 从数据库中查找当前购物车.(查询)
  3. 如果数据库中不存在当前购物车,请创建新的购物车实体(在内存中).
  4. 将新项目(产品)及其价格添加到购物车实体(内存中).
  5. 在整个购物车上运行任何折扣计算.(取决于查询)
  6. 在购物车上运行任何销售税计算.(取决于查询)
  7. 在购物车上运行任何运费计算.(取决于查询)
  8. 如果这是一个新的购物车,请将实体添加到数据库,否则更新数据库中的购物车.(命令)

因此,虽然"AddToCart"听起来应该是一个命令(因为它更新了系统状态),但实际上它依赖于许多查询.

我的问题

处理这样的工作流程的普遍接受的方法是什么?

  1. 制作一个AddToCartCommandHandler取决于可能运行查询的其他服务.
  2. 创建一个Facade CartService,用于协调运行查询的工作流,然后是命令.
  3. 使控制器操作方法首先运行查询,然后运行任何命令.如果需要重复使用,可能会错过某些查询步骤.
  4. 其他?

我之所以无法找到答案,是因为它"取决于设计",这是不适用的例外情况之一吗?

如果命令和查询是分开的,我是否会将我的真实实体框架实体类传递给添加/更新购物车的命令(因此EF可以确定它是否附加)?在这种情况下,似乎DTO不会这样做.

注意:我隐含地假设实现的系统这样CQS做的目的是最终它们可以成为一个完整的CQRS系统.如果是这样,这个工作流程显然无法进行转换 - 因此我的问题.

背景

我正在第一次尝试CQS.

从我阅读的有关此模式的文档中可以清楚地看出,查询不得更改系统状态.

但是,目前还不清楚是否可以从命令中运行查询(我似乎无法在任何地方找到任何信息).

我可以想到几个真实案例需要发生的事情.但是,鉴于在线缺乏这种模式的现实例子,我不确定如何继续.在线有很多理论,但我能找到的唯一代码就是这里和这里.

Nig*_*888 5

这个问题的答案以qujck 的评论的形式出现。

解决方案是将应用程序分为不同的查询类型和命令类型。每种类型的确切目的仍然是个谜(因为博客文章没有详细说明他做出这种区分的原因),但它确实清楚地表明了顶级和中级命令如何依赖于数据库查询。

命令类型

  1. 命令(顶层)
  2. 指挥策略(中级)
  3. 数据命令(直接数据访问)

查询类型

  1. 查询(顶级)
  2. 查询策略(中级)
  3. 数据查询(直接数据访问)

命令查询实现

// Commands

public interface ICommand
{
}

public interface IDataCommand
{
}

/// <summary>
/// A holistic abstraction, an abstraction that acts as the whole of each transaction 
/// </summary>
/// <typeparam name="TCommand"></typeparam>
public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}

public interface ICommandStrategyHandler<TCommand> where TCommand : ICommand
{
    void Handle(TCommand command);
}

/// <summary>
/// Direct database update
/// </summary>
/// <typeparam name="TCommand"></typeparam>
public interface IDataCommandHandler<TCommand> where TCommand : IDataCommand
{
    void Handle(TCommand command);
}



// Queries

public interface IQuery<TResult>
{
}

public interface IDataQuery<TResult>
{
}

/// <summary>
/// A holistic abstraction, an abstraction that acts as the whole of each transaction 
/// </summary>
/// <typeparam name="TQuery"></typeparam>
/// <typeparam name="TResult"></typeparam>
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

public interface IQueryStrategyHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

/// <summary>
/// Direct database query
/// </summary>
/// <typeparam name="TQuery"></typeparam>
/// <typeparam name="TResult"></typeparam>
public interface IDataQueryHandler<TQuery, TResult> where TQuery : IDataQuery<TResult>
{
    TResult Handle(TQuery query);
}


/// <summary>
/// Generic processor that can run any query
/// </summary>
public interface IQueryProcessor
{
    TResult Execute<TResult>(IQuery<TResult> query);

    // NOTE: Stephen recommends against using Async. He may be right that it is not
    // worth the aggrevation of bugs that may be introduced.
    //Task<TResult> Execute<TResult>(IQuery<TResult> query);

    TResult Execute<TResult>(IDataQuery<TResult> query);
}
Run Code Online (Sandbox Code Playgroud)

AddToCart 依赖关系图

使用上述实现,AddToCart 工作流依赖关系图的结构如下所示。

  • AddToCartCommandHandler : ICommandHandler<AddToCartCommand>
    • GetShoppingCartDetailsQueryHandler : IQueryHandler<GetShoppingCartDetailsQuery, ShoppingCartDetails>
      • GetShoppingCartQueryStrategyHandler : IQueryStrategyHandler<GetShoppingCartQueryStrategy, ShoppingCartDetails>
        • GetShoppingCartDataQueryHandler : IDataQueryHandler<GetShoppingCartDataQuery, ShoppingCartDetails>
          • ApplicationDbContext
        • CreateShoppingCartDataCommandHandler : IDataCommandHandler<CreateShoppingCartDataCommand>
          • ApplicationDbContext
    • UpdateShoppingCartDataCommandHandler : IDataCommandHandler<UpdateShoppingCartDataCommand>
    • SetItemPriceCommandStrategyHandler : ICommandStrategyHandler<SetItemPriceCommandStrategy>
      • GetProductDetailsDataQueryHandler : IDataQueryHandler<GetProductDetailsDataQuery, ProductDetails>
        • ApplicationDbContext
    • SetTotalsCommandStrategyHandler : ICommandStrategyHandler<SetTotalsCommandStrategy>
      • SetDiscountsCommandStrategyHandler : ICommandStrategyHandler<SetDiscountsCommandStrategy>
        • ?
      • SetSalesTaxCommandStrategyHandler : ICommandStrategyHandler<SetSalesTaxCommandStrategy>

执行

DTO

public class ShoppingCartDetails : IOrder
{
    private IEnumerable<IOrderItem> items = new List<ShoppingCartItem>();

    public Guid Id { get; set; }
    public decimal SubtotalDiscounts { get; set; }
    public string ShippingPostalCode { get; set; }
    public decimal Shipping { get; set; }
    public decimal ShippingDiscounts { get; set; }
    public decimal SalesTax { get; set; }
    public decimal SalesTaxDiscounts { get; set; }

    // Declared twice - once for the IOrder interface
    // and once so we can get the realized concrete type.
    // See: /sf/ask/1084344341/
    public IEnumerable<ShoppingCartItem> Items
    {
        get { return this.items as IEnumerable<ShoppingCartItem>; }
        set { this.items = value; }
    }
    IEnumerable<IOrderItem> IOrder.Items
    {
        get { return this.items; }
        set { this.items = value; }
    }

    //public IEnumerable<ShoppingCartNotification> Notifications { get; set; }
    //public IEnumerable<ShoppingCartCoupon> Coupons { get; set; } // TODO: Add this to IOrder
}

public class ShoppingCartItem : IOrderItem
{
    public ShoppingCartItem()
    {
        this.Id = Guid.NewGuid();
        this.Selections = new Dictionary<string, object>();
    }

    public Guid Id { get; set; }
    public Guid ShoppingCartId { get; set; }
    public Guid ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
    public decimal PriceDiscount { get; set; }
    public IDictionary<string, object> Selections { get; set; }
}

public class ProductDetails 
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public decimal Discount { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

计算订单总计

我没有依赖一系列服务来执行简单(且必需)的算术,而是选择将此行为放入扩展方法中,以便根据实际数据即时完成。由于此逻辑需要在购物车、订单和报价之间共享,因此计算是针对IOrder而IOrderItem不是具体模型类型完成的。

// Contract to share simple cacluation and other business logic between shopping cart, order, and quote
public interface IOrder
{
    decimal SubtotalDiscounts { get; set; }
    decimal Shipping { get; set; }
    decimal ShippingDiscounts { get; set; }
    decimal SalesTax { get; set; }
    decimal SalesTaxDiscounts { get; set; }
    IEnumerable<IOrderItem> Items { get; set; }
}

public interface IOrderItem
{
    Guid ProductId { get; set; }
    int Quantity { get; set; }
    decimal Price { get; set; }
    decimal PriceDiscount { get; set; }
    IDictionary<string, object> Selections { get; set; }
}

public static class OrderExtensions
{
    public static decimal GetSubtotal(this IOrder order)
    {
        return order.Items.Sum(x => x.GetTotal());
    }

    public static decimal GetSubtotalBeforeDiscounts(this IOrder order)
    {
        return order.Items.Sum(x => x.GetTotalBeforeDiscounts());
    }

    public static decimal GetTotal(this IOrder order)
    {
        var subtotal = (order.GetSubtotal() - order.SubtotalDiscounts);
        var shipping = (order.Shipping - order.ShippingDiscounts);
        var salesTax = (order.SalesTax - order.SalesTaxDiscounts);
        return (subtotal + shipping + salesTax);
    }
}

public static class OrderItemExtensions
{
    public static decimal GetTotalBeforeDiscounts(this IOrderItem item)
    {
        return (item.Price * item.Quantity);
    }

    public static decimal GetTotal(this IOrderItem item)
    {
        return (GetTotalBeforeDiscounts(item) - item.PriceDiscount);
    }

    public static decimal GetDiscountedUnitPrice(this IOrderItem item)
    {
        return (item.Quantity > 0) ? (GetTotal(item) / item.Quantity) : 0;
    }
}
Run Code Online (Sandbox Code Playgroud)

购物车控制器

为简洁起见,我们仅显示 AddToCart 操作,但这也是针对购物车的其他操作(即从购物车中删除)的地方。

public class ShoppingCartController : Controller
{
    private readonly IQueryProcessor queryProcessor;
    private readonly IAnonymousIdAccessor anonymousIdAccessor;
    private readonly ICommandHandler<AddToCartCommand> addToCartHandler;

    public ShoppingCartController(
        IQueryProcessor queryProcessor, 
        IAnonymousIdAccessor anonymousIdAccessor, 
        ICommandHandler<AddToCartCommand> addToCartHandler)
    {
        if (queryProcessor == null)
            throw new ArgumentNullException("queryProcessor");
        if (anonymousIdAccessor == null)
            throw new ArgumentNullException("anonymousIdAccessor");
        if (addToCartHandler == null)
            throw new ArgumentNullException("addToCartHandler");

        this.queryProcessor = queryProcessor;
        this.anonymousIdAccessor = anonymousIdAccessor;
        this.addToCartHandler = addToCartHandler;
    }

    public ActionResult Index()
    {
        var command = new GetShoppingCartDetailsQuery
        {
            ShoppingCartId = this.anonymousIdAccessor.AnonymousID
        };

        ShoppingCartDetails cart = this.queryProcessor.Execute(command);

        return View(cart);
    }

    public ActionResult AddToCart(ItemViewModel model)
    {
        var command = new AddToCartCommand
        {
            ProductId = model.Id,
            Quantity = model.Qty,
            Selections = model.Selections,
            ShoppingCartId = this.anonymousIdAccessor.AnonymousID
        };

        this.addToCartHandler.Handle(command);

        // If we execute server side, it should go to the cart page
        return RedirectToAction("Index");
    }
}
Run Code Online (Sandbox Code Playgroud)

添加到购物车命令处理程序

这是工作流程的主要部分执行的地方。该命令将直接从控制器操作调用AddToCart。

public class AddToCartCommandHandler : ICommandHandler<AddToCartCommand>
{
    private readonly IQueryStrategyHandler<GetShoppingCartQueryStrategy, ShoppingCartDetails> getShoppingCartQuery;
    private readonly IDataCommandHandler<UpdateShoppingCartDataCommand> updateShoppingCartCommand;
    private readonly ICommandStrategyHandler<SetItemPriceCommandStrategy> setItemPriceCommand;
    private readonly ICommandStrategyHandler<SetTotalsCommandStrategy> setTotalsCommand;

    public AddToCartCommandHandler(
        IQueryStrategyHandler<GetShoppingCartQueryStrategy, ShoppingCartDetails> getShoppingCartCommand,
        IDataCommandHandler<UpdateShoppingCartDataCommand> updateShoppingCartCommand,
        ICommandStrategyHandler<SetItemPriceCommandStrategy> setItemPriceCommand,
        ICommandStrategyHandler<SetTotalsCommandStrategy> setTotalsCommand
        )
    {
        if (getShoppingCartCommand == null)
            throw new ArgumentNullException("getShoppingCartCommand");
        if (setItemPriceCommand == null)
            throw new ArgumentNullException("setItemPriceCommand");
        if (updateShoppingCartCommand == null)
            throw new ArgumentNullException("updateShoppingCartCommand");
        if (setTotalsCommand == null)
            throw new ArgumentNullException("setTotalsCommand");

        this.getShoppingCartQuery = getShoppingCartCommand;
        this.updateShoppingCartCommand = updateShoppingCartCommand;
        this.setItemPriceCommand = setItemPriceCommand;
        this.setTotalsCommand = setTotalsCommand;
    }

    public void Handle(AddToCartCommand command)
    {
        // Get the shopping cart (aggregate root) from the database
        var shoppingCart = getShoppingCartQuery.Handle(new GetShoppingCartQueryStrategy { ShoppingCartId = command.ShoppingCartId });

        // Create a new shopping cart item
        var item = new Contract.DTOs.ShoppingCartItem
        {
            ShoppingCartId = command.ShoppingCartId,
            ProductId = command.ProductId,
            Quantity = command.Quantity,

            // Dictionary representing the option selections the user made on the UI
            Selections = command.Selections
        };

        // Set the item's price (calculated/retrieved from database query)
        setItemPriceCommand.Handle(new SetItemPriceCommandStrategy { ShoppingCartItem = item });

        // Add the item to the cart
        var items = new List<Contract.DTOs.ShoppingCartItem>(shoppingCart.Items);
        items.Add(item);
        shoppingCart.Items = items;

        // Set the shopping cart totals (sales tax, discounts)
        setTotalsCommand.Handle(new SetTotalsCommandStrategy { ShoppingCart = shoppingCart });

        // Update the shopping cart details in the database
        updateShoppingCartCommand.Handle(new UpdateShoppingCartDataCommand { ShoppingCart = shoppingCart });
    }
}
Run Code Online (Sandbox Code Playgroud)

获取购物车查询策略处理程序

public class GetShoppingCartQueryStrategyHandler : IQueryStrategyHandler<GetShoppingCartQueryStrategy, ShoppingCartDetails>
{
    private readonly IDataQueryHandler<GetShoppingCartDataQuery, ShoppingCartDetails> getShoppingCartDataQuery;
    private readonly IDataCommandHandler<CreateShoppingCartDataCommand> createShoppingCartDataCommand;

    public GetShoppingCartQueryStrategyHandler(
        IDataQueryHandler<GetShoppingCartDataQuery, ShoppingCartDetails> getShoppingCartDataQuery,
        IDataCommandHandler<CreateShoppingCartDataCommand> createShoppingCartDataCommand)
    {
        if (getShoppingCartDataQuery == null)
            throw new ArgumentNullException("getShoppingCartDataQuery");
        if (createShoppingCartDataCommand == null)
            throw new ArgumentNullException("createShoppingCartDataCommand");

        this.getShoppingCartDataQuery = getShoppingCartDataQuery;
        this.createShoppingCartDataCommand = createShoppingCartDataCommand;
    }

    public ShoppingCartDetails Handle(GetShoppingCartQueryStrategy query)
    {
        var result = this.getShoppingCartDataQuery.Handle(new GetShoppingCartDataQuery { ShoppingCartId = query.ShoppingCartId });

        // If there is no shopping cart, create one.
        if (result == null)
        {
            this.createShoppingCartDataCommand.Handle(new CreateShoppingCartDataCommand { ShoppingCartId = query.ShoppingCartId });

            result = new ShoppingCartDetails
            {
                Id = query.ShoppingCartId
            };
        }

        return result;
    }
}
Run Code Online (Sandbox Code Playgroud)

获取购物车数据查询处理程序

/// <summary>
/// Data handler to get the shopping cart data (if it exists)
/// </summary>
public class GetShoppingCartDataQueryHandler : IDataQueryHandler<GetShoppingCartDataQuery, ShoppingCartDetails>
{
    private readonly IAppContext context;

    public GetShoppingCartDataQueryHandler(IAppContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");
        this.context = context;
    }

    public ShoppingCartDetails Handle(GetShoppingCartDataQuery query)
    {
        return (from shoppingCart in context.ShoppingCarts
                where shoppingCart.Id == query.ShoppingCartId
                select new ShoppingCartDetails
                {
                    Id = shoppingCart.Id,
                    SubtotalDiscounts = shoppingCart.SubtotalDiscounts,
                    ShippingPostalCode = shoppingCart.ShippingPostalCode,
                    Shipping = shoppingCart.Shipping,
                    ShippingDiscounts = shoppingCart.ShippingDiscounts,
                    SalesTax = shoppingCart.SalesTax,
                    SalesTaxDiscounts = shoppingCart.SalesTaxDiscounts,

                    Items = shoppingCart.Items.Select(i =>
                        new Contract.DTOs.ShoppingCartItem
                        {
                            Id = i.Id,
                            ShoppingCartId = i.ShoppingCartId,
                            ProductId = i.ProductId,
                            Quantity = i.Quantity,
                            Price = i.Price,
                            PriceDiscount = i.PriceDiscount
                            // TODO: Selections...
                        })
                }).FirstOrDefault();
    }
}
Run Code Online (Sandbox Code Playgroud)

创建购物车数据命令处理程序

public class CreateShoppingCartDataCommandHandler : IDataCommandHandler<CreateShoppingCartDataCommand>
{
    private readonly IAppContext context;

    public CreateShoppingCartDataCommandHandler(IAppContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");
        this.context = context;
    }

    public void Handle(CreateShoppingCartDataCommand command)
    {
        var cart = new ShoppingCart
        {
            Id = command.ShoppingCartId
        };

        this.context.ShoppingCarts.Add(cart);
        this.context.SaveChanges();
    }
}
Run Code Online (Sandbox Code Playgroud)

更新购物车数据命令处理程序

这将使用业务层应用的所有更改更新购物车。

目前,这个“命令”执行查询,以便它可以协调数据库和内存副本之间的差异。然而,这显然违反了CQS模式。我计划提出一个后续问题,以确定变更跟踪的最佳行动方案是什么,因为变更跟踪和 CQS 似乎密切相关。

public class UpdateShoppingCartDataCommandHandler : IDataCommandHandler<UpdateShoppingCartDataCommand>
{
    private readonly IAppContext context;

    public UpdateShoppingCartDataCommandHandler(IAppContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");
        this.context = context;
    }

    public void Handle(UpdateShoppingCartDataCommand command)
    {
        var cart = context.ShoppingCarts
            .Include(x => x.Items)
            .Single(x => x.Id == command.ShoppingCart.Id);


        cart.Id = command.ShoppingCart.Id;
        cart.SubtotalDiscounts = command.ShoppingCart.SubtotalDiscounts;
        cart.ShippingPostalCode = command.ShoppingCart.ShippingPostalCode;
        cart.Shipping = command.ShoppingCart.Shipping;
        cart.ShippingDiscounts = command.ShoppingCart.ShippingDiscounts;
        cart.SalesTax = command.ShoppingCart.SalesTax;
        cart.SalesTaxDiscounts = command.ShoppingCart.SalesTaxDiscounts;

        ReconcileShoppingCartItems(cart.Items, command.ShoppingCart.Items, command.ShoppingCart.Id);

        // Update the cart with new data

        context.SaveChanges();
    }

    private void ReconcileShoppingCartItems(ICollection<ShoppingCartItem> items, IEnumerable<Contract.DTOs.ShoppingCartItem> itemDtos, Guid shoppingCartId)
    {
        // remove deleted items
        var items2 = new List<ShoppingCartItem>(items);
        foreach (var item in items2)
        {
            if (!itemDtos.Any(x => x.Id == item.Id))
            {
                context.Entry(item).State = EntityState.Deleted;
            }
        }


        // Add/update items
        foreach (var dto in itemDtos)
        {
            var item = items.FirstOrDefault(x => x.Id == dto.Id);
            if (item == null)
            {
                items.Add(new ShoppingCartItem
                {
                    Id = Guid.NewGuid(),
                    ShoppingCartId = shoppingCartId,
                    ProductId = dto.ProductId,
                    Quantity = dto.Quantity,
                    Price = dto.Price,
                    PriceDiscount = dto.PriceDiscount
                });
            }
            else
            {
                item.ProductId = dto.ProductId;
                item.Quantity = dto.Quantity;
                item.Price = dto.Price;
                item.PriceDiscount = dto.PriceDiscount;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

SetItemPriceCommandStrategyHandler

public class SetItemPriceCommandStrategyHandler : ICommandStrategyHandler<SetItemPriceCommandStrategy>
{
    private readonly IDataQueryHand