asp.net-mvc - 从命令运行查询是否违反了命令-查询分隔?

标签 asp.net-mvc entity-framework design-patterns command-query-separation

给定现实世界中的匿名购物车,“AddToCart”工作流程必须执行以下步骤:

  • 从数据库中查找当前产品。从产品获取价格或使用服务来计算用户选择和其他产品属性的价格。 (查询)
  • 从数据库中查找当前的购物车。 (查询)
  • 如果数据库中不存在当前购物车,请在内存中创建一个新的购物车实体。
  • 将新商品(产品)及其价格添加到购物车实体(在内存中)。
  • 在整个购物车上运行所有折扣计算。 (取决于查询)
  • 在购物车上运行所有营业税计算。 (取决于查询)
  • 在购物车上运行所有运费计算。 (取决于查询)
  • 如果这是新的购物车,则将实体添加到数据库,否则更新数据库中的购物车。 (命令)

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

    我的问题

    处理这种工作流程的普遍接受的方法是什么?
  • 生成AddToCartCommandHandler,它取决于可能运行查询的其他服务。
  • 制作外观CartService,以协调首先运行查询和命令的工作流程。
  • 使 Controller 操作方法首先运行查询,然后运行任何命令。如果需要重新使用某些查询步骤,可能会错过一些查询步骤。
  • 其他吗?

  • 是我找不到答案的原因,因为它“取决于设计”,这是不应用它的异常(exception)之一?

    如果命令和查询是分开的,我可以将我的真实 Entity Framework 实体类传递给添加/更新购物车的命令(以便EF可以算出是否已附加购物车)吗?在这种情况下,DTO似乎无法使用。

    NOTE: I am implicitly assuming that systems that implement CQS do so with the aim that eventually they could become a full-on CQRS system. If so, this workflow apparently would not be able to make the transition - hence my question.



    背景

    我要去CQS的第一场刺。

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

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

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

    最佳答案

    这个问题的答案来自qujck的a comment形式。

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

    命令类型

  • 命令(顶层)
  • 命令策略(中级)
  • 数据命令(直接数据访问)

  • 查询类型
  • 查询(顶级)
  • 查询策略(中级)
  • 数据查询(直接数据访问)

  • 命令查询实现
    // 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;
        }
    }
    

    ShoppingCartController

    为简便起见,我们仅显示AddToCart Action ,但这也是针对购物车的其他 Action (即从购物车中删除)也将进行的地方。
    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 Controller 操作中调用。
    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之后发生。可能需要一些时间。

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

    但是,感觉上这实际上只是从概念上将查询与命令分开。从物理上讲,它们仍然彼此依赖,除非您仅查看仅依赖IDataCommandIDataQueryApplicationDbContext抽象。我不确定这是否是qujck的意图。我也不确定这是否解决了设计是否可以转移到CQRS的更大问题,但是由于这不是我计划的内容,因此我对此并不担心。

    关于asp.net-mvc - 从命令运行查询是否违反了命令-查询分隔?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36578997/

    相关文章:

    ajax - Ajax错误消息仅显示状态

    java - Decorator模式链式方法如何调用?

    java - 单例实现示例

    c# - catch 语句中的附加 try 语句 - 代码味道?

    asp.net - ViewBag 名称可以与 DropDownList 中的 Model 属性名称相同吗?

    c# - 运算符 '>' 不能应用于查询中类型为 'string' 和 'string' 的操作数

    c# - Entity Framework - 填充子对象,过滤子对象

    c# - 具有AutoMapper的体系结构?

    javascript - 选择更改不起作用

    c# - edmx 文件在单独的项目中时出错