>从数据库中查找当前产品.从产品中获取价格或使用服务来计算用户选择和其他产品属性的价格. (查询)
>从数据库中查找当前购物车. (查询)
>如果数据库中不存在当前购物车,请创建新的购物车实体(在内存中).
>将新项目(产品)及其价格添加到购物车实体(内存中).
>在整个购物车上运行任何折扣计算. (取决于查询)
>在购物车上运行任何销售税计算. (取决于查询)
>在购物车上运行任何运费计算. (取决于查询)
>如果这是一个新的购物车,请将实体添加到数据库,否则更新数据库中的购物车. (命令)
因此,虽然“AddToCart”听起来应该是一个命令(因为它更新了系统状态),但实际上它依赖于许多查询.
我的问题
处理这样的工作流程的普遍接受的方法是什么?
>创建一个依赖于可能运行查询的其他服务的AddToCartCommandHandler.
>创建一个Facade CartService,它首先编排运行查询的工作流,然后是命令.
>使控制器操作方法首先运行查询,然后运行任何命令.如果需要重复使用,可能会错过某些查询步骤.
>其他?
我之所以无法找到答案,是因为它“取决于设计”,这是不适用的例外情况之一吗?
如果命令和查询是分开的,我是否会将我的真实实体框架实体类传递给添加/更新购物车的命令(因此EF可以确定它是否附加)?在这种情况下,似乎DTO不会这样做.
NOTE: I am implicitly assuming that systems that implement
CQS
do so with the aim that eventually they Could become a full-onCQRS
system. If so,this workflow apparently would not be able to make the transition – hence my question.
背景
我正在第一次尝试CQS.
从我阅读的有关此模式的文档中可以清楚地看出,查询不得更改系统状态.
但是,目前还不清楚是否可以从命令中运行查询(我似乎无法在任何地方找到任何信息).
我可以想到几个真实案例需要发生的事情.但是,鉴于在线缺乏这种模式的现实例子,我不确定如何继续.在线有很多理论,但我能找到的唯一代码是here和here.
解决方法
解决方案是将应用程序分解为不同的查询类型和命令类型.每种类型的确切目的仍然是一个谜(因为博客文章没有说明他为何做出这种区分的原因),但它确实清楚地表明顶级和中级命令如何依赖于数据库查询.
命令类型
>命令(顶级)
>指挥战略(中级)
>数据命令(直接数据访问)
查询类型
命令查询实现
// 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; } }
计算订单总数
我没有依赖一串服务来做简单(和必需)的算法,而是选择将这种行为放入扩展方法中,以便它可以在实际数据中动态完成.由于此逻辑需要在购物车,订单和报价之间共享,因此计算是针对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; } }
ShoppingCartController
为简洁起见,我们仅显示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工作流程设置为在添加项目时立即运行的步骤,并使得在从其(可能是外部)源检索到总计之后再次更新UI的事实之后发生CalculateShippingAndTax工作流.可能需要时间.
这会解决问题吗?是的,它确实解决了当命令需要依赖查询时我遇到的现实问题.
但是,感觉这只是在概念上将查询与命令分开.在物理上,它们仍然相互依赖,除非您只查看仅依赖于ApplicationDbContext的IDataCommand和IDataQuery抽象.我不确定这是否是qujck的意图.我也不确定这是否解决了设计可转移到CQRS的更大问题,但因为它不是我计划的东西,所以我并不关心它.
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。