6

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

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

因此,尽管“AddToCart”听起来应该是一个命令(因为它会更新系统状态),但实际上它依赖于许多查询。

我的问题

处理此类工作流的普遍接受的方法是什么?

  1. 创建一个AddToCartCommandHandler依赖于可能运行查询的其他服务。
  2. 制作一个外观CartService来编排首先运行查询的工作流,然后是命令。
  3. 使控制器操作方法首先运行查询,然后运行任何命令。如果需要重用,似乎可能会错过一些查询步骤。
  4. 其他?

我找不到答案的原因是因为它“取决于设计”,这是不应用它的例外情况之一?

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

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

背景

我正在 CQS 进行我的第一次尝试。

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

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

我可以想到几个现实世界的案例需要发生这种情况。但是,鉴于在线上缺乏这种模式的真实示例,我不确定如何进行。网上有很多理论,但我能找到的唯一代码是herehere

4

2 回答 2

4

这个问题的答案来自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);
}

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: https://stackoverflow.com/questions/15490633/why-cant-i-use-a-compatible-concrete-type-when-implementing-an-interface
    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; }
}

计算订单总计

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

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

购物车控制器

为简洁起见,我们只展示了 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");
    }
}

AddToCartCommandHandler

这是执行工作流的主要部分的地方。此命令将直接从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 });
    }
}

GetShoppingCartQueryStrategyHandler

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

GetShoppingCartDataQueryHandler

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

CreateShoppingCartDataCommandHandler

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

UpdateShoppingCartDataCommandHandler

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

目前,这个“命令”会进行查询,以便协调数据库和内存副本之间的差异。但是,这显然违反了 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;
            }
        }
    }
}

SetItemPriceCommandStrategyHandler

public class SetItemPriceCommandStrategyHandler : ICommandStrategyHandler<SetItemPriceCommandStrategy>
{
    private readonly IDataQueryHandler<GetProductDetailsDataQuery, ProductDetails> getProductDetailsQuery;

    public SetItemPriceCommandStrategyHandler(
        IDataQueryHandler<GetProductDetailsDataQuery, ProductDetails> getProductDetailsQuery)
    {
        if (getProductDetailsQuery == null)
            throw new ArgumentNullException("getProductDetailsQuery");

        this.getProductDetailsQuery = getProductDetailsQuery;
    }

    public void Handle(SetItemPriceCommandStrategy command)
    {
        var shoppingCartItem = command.ShoppingCartItem;

        var product = getProductDetailsQuery.Handle(new GetProductDetailsDataQuery { ProductId = shoppingCartItem.ProductId });

        // TODO: For products with custom calculations, need to use selections on shopping cart item
        // as well as custom formula and pricing points from product to calculate the item price.

        shoppingCartItem.Price = product.Price;
    }
}

GetProductDetailsDataQueryHandler

public class GetProductDetailsDataQueryHandler : IDataQueryHandler<GetProductDetailsDataQuery, ProductDetails>
{
    private readonly IAppContext context;

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

    public ProductDetails Handle(GetProductDetailsDataQuery query)
    {
        return (from product in context.Products
                where product.Id == query.ProductId
                select new ProductDetails
                {
                    Id = product.Id,
                    Name = product.Name,
                    Price = product.Price
                }).FirstOrDefault(); 
    }
}

SetTotalsCommandStrategyHandler

public class SetTotalsCommandStrategyHandler : ICommandStrategyHandler<SetTotalsCommandStrategy>
{
    private readonly ICommandStrategyHandler<SetDiscountsCommandStrategy> setDiscountsCommand;
    private readonly ICommandStrategyHandler<SetSalesTaxCommandStrategy> setSalesTaxCommand;

    public SetTotalsCommandStrategyHandler(
        ICommandStrategyHandler<SetDiscountsCommandStrategy> setDiscountsCommand,
        ICommandStrategyHandler<SetSalesTaxCommandStrategy> setSalesTaxCommand
        )
    {
        if (setDiscountsCommand == null)
            throw new ArgumentNullException("setDiscountsCommand");
        if (setSalesTaxCommand == null)
            throw new ArgumentNullException("setSalesTaxCommand");

        this.setDiscountsCommand = setDiscountsCommand;
        this.setSalesTaxCommand = setSalesTaxCommand;
    }

    public void Handle(SetTotalsCommandStrategy command)
    {
        var shoppingCart = command.ShoppingCart;

        // Important: Discounts must be calculated before sales tax to ensure the discount is applied
        // to the subtotal before tax is calculated.
        setDiscountsCommand.Handle(new SetDiscountsCommandStrategy { ShoppingCart = shoppingCart });
        setSalesTaxCommand.Handle(new SetSalesTaxCommandStrategy { ShoppingCart = shoppingCart });
    }
}

SetDiscountsCommandStrategyHandler

public class SetDiscountsCommandStrategyHandler : ICommandStrategyHandler<SetDiscountsCommandStrategy>
{
    public void Handle(SetDiscountsCommandStrategy command)
    {
        var shoppingCart = command.ShoppingCart;

        // TODO: Set discounts according to business rules

        foreach (var item in shoppingCart.Items)
        {
            item.PriceDiscount = 0;
        }

        shoppingCart.SubtotalDiscounts = 0;
        shoppingCart.SalesTaxDiscounts = 0;
        shoppingCart.ShippingDiscounts = 0;
    }
}

SetSalesTaxCommandStrategyHandler

public class SetSalesTaxCommandStrategyHandler : ICommandStrategyHandler<SetSalesTaxCommandStrategy>
{
    public void Handle(SetSalesTaxCommandStrategy command)
    {
        var shoppingCart = command.ShoppingCart;
        var postalCode = command.ShoppingCart.ShippingPostalCode;

        bool isInCalifornia = !string.IsNullOrEmpty(postalCode) ?
            // Matches 90000 to 96200
            Regex.IsMatch(postalCode, @"^9(?:[0-5]\d{3}|6[0-1]\d{2}|6200)(?:-?(?:\d{4}))?$") :
            false;

        if (isInCalifornia)
        {
            var subtotal = shoppingCart.GetSubtotal();

            // Rule for California - charge a flat 7.75% if the zip code is in California
            var salesTax = subtotal * 0.0775M;

            shoppingCart.SalesTax = salesTax;
        }
    }
}

请注意,此工作流程中没有运费计算。这主要是因为运费计算可能取决于外部 API,并且可能需要一些时间才能返回。因此,我计划使AddToCart工作流程成为一个在添加项目时立即运行的步骤,并在CalculateShippingAndTax从其(可能是外部)来源检索总计后再次更新 UI 之后发生工作流程,这可能需要时间。

这能解决问题吗?是的,它确实解决了我在命令需要依赖查询时遇到的实际问题。

但是,感觉这实际上只是在概念上将查询与命令分开。从物理上讲,它们仍然相互依赖,除非您只查看仅依赖于的IDataCommand和抽象。我不确定这是否是 qujck 的意图。我也不确定这是否解决了设计是否可以转移到 CQRS 的更大问题,但由于这不是我计划的事情,所以我并不关心它。IDataQueryApplicationDbContext

于 2016-04-28T10:48:20.833 回答
-1

在相互冲突的设计原则之间总是需要权衡取舍。解决它的方法是查看这些原则背后的根本原因。在这种情况下,不运行命令就无法运行查询是有问题的,但是不运行查询就无法运行命令通常是无害的。只要有一种方法可以独立运行查询,我认为没有理由不将查询结果添加到命令中,尤其是在执行以下操作时:

QueryResult command()
{
   // do command stuff
   return query();
}
于 2016-04-28T02:59:02.713 回答