Given a real-world anonymous shopping cart, the "AddToCart" workflow must do the following steps:
鉴于真实的匿名购物车,“AddToCart”工作流程必须执行以下步骤:
- Lookup the current product from the database. Get the price from the product or use a service to calculate the price on user selections and other product properties. (query)
- Lookup the current shopping cart from the database. (query)
- If the current shopping cart doesn't exist in the database, create a new shopping cart entity (in memory).
- Add the new item (product) to the shopping cart entity (in memory) along with its price.
- Run any discount calculations on the entire shopping cart. (depends on query)
- Run any sales tax calculations on the shopping cart. (depends on query)
- Run any shipping calculations on the shopping cart. (depends on query)
- If this is a new shopping cart, add the entity to the database, otherwise update the shopping cart in the database. (command)
从数据库中查找当前产品。从产品中获取价格或使用服务来计算用户选择和其他产品属性的价格。 (查询)
从数据库中查找当前购物车。 (查询)
如果数据库中不存在当前购物车,请创建新的购物车实体(在内存中)。
将新项目(产品)及其价格添加到购物车实体(内存中)。
在整个购物车上运行任何折扣计算。 (取决于查询)
在购物车上运行任何销售税计算。 (取决于查询)
在购物车上运行任何运费计算。 (取决于查询)
如果这是一个新的购物车,请将实体添加到数据库,否则更新数据库中的购物车。 (命令)
So, although "AddToCart" sounds like it should be a command (since it updates the system state), in practice it depends on many queries.
因此,虽然“AddToCart”听起来应该是一个命令(因为它更新系统状态),但实际上它依赖于许多查询。
My Question
What is the generally accepted way to handle workflows like this?
处理这样的工作流程的普遍接受的方法是什么?
- Make an
AddToCartCommandHandler
that depends on other services that may run queries. - Make a facade
CartService
that orchestrates the workflow that runs the queries first followed by the commands. - Make the controller action method first run the queries, then run any commands. Seems like some of the query steps could be missed if this needs to be reused.
- Other?
创建一个依赖于可能运行查询的其他服务的AddToCartCommandHandler。
创建一个Facade CartService,它首先编排运行查询的工作流,然后是命令。
使控制器操作方法首先运行查询,然后运行任何命令。似乎如果需要重复使用某些查询步骤可能会错过。
Is the reason I can't find an answer about this because it "depends on the design" and this is one of the exceptions where not to apply it?
是我无法找到答案的原因,因为它“取决于设计”,这是不适用它的例外情况之一?
If the commands and queries are separated, would I pass my real entity framework entity class to the command that adds/updates the cart (so EF can work out whether it is attached or not)? It seems like a DTO won't do in this case.
如果命令和查询是分开的,我是否会将我的真实实体框架实体类传递给添加/更新购物车的命令(因此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的系统这样做的目的是最终它们可以成为一个全面的CQRS系统。如果是这样,这个工作流程显然无法进行转换 - 因此我的问题。
Background
I am taking my first stab at CQS.
我正在第一次尝试CQS。
It is clear from the documentation I have read about this pattern that a query must not change the system state.
从我阅读的有关此模式的文档中可以清楚地看出,查询不得更改系统状态。
However, it is unclear whether it is considered okay to run a query from within a command (I can't seem to find any info anywhere).
但是,目前还不清楚是否可以从命令中运行查询(我似乎无法在任何地方找到任何信息)。
There are several real-world cases I can think of where this needs to happen. But, given the lack of real-world examples of this pattern online I am uncertain how to proceed. There is lots of theory online, but the only code I can find is here and here.
有几个真实世界的案例,我可以想到这需要发生的地方。但是,鉴于在线缺乏这种模式的现实例子,我不确定如何继续。网上有很多理论,但我能找到的唯一代码就是这里和这里。
2 个解决方案
#1
4
The answer to this problem came in the form of a comment by qujck.
这个问题的答案来自qujck的评论。
The solution is to break the application into different query types and command types. The exact purpose of each type remain a mystery (since the blog post doesn't go into the reasons why he made this distinction), but it does make it clear how top-level and mid-level commands can depend on database queries.
解决方案是将应用程序分解为不同的查询类型和命令类型。每种类型的确切目的仍然是一个谜(因为博客文章没有说明他之所以做出这种区分的原因),但它确实清楚地表明*和中级命令如何依赖于数据库查询。
Command Types
- Command (top-level)
- Command Strategy (mid-level)
- Data Command (direct data access)
指挥战略(中级)
数据命令(直接数据访问)
Query Types
- Query (top-level)
- Query Strategy (mid-level)
- Data Query (direct data access)
查询策略(中级)
数据查询(直接数据访问)
Command-Query Implementation
// 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 Dependency Graph
Using the above implementation, the structure of the AddToCart workflow dependency graph looks like this.
使用上面的实现,AddToCart工作流依赖关系图的结构如下所示。
-
AddToCartCommandHandler : ICommandHandler<AddToCartCommand>
-
GetShoppingCartDetailsQueryHandler : IQueryHandler<GetShoppingCartDetailsQuery, ShoppingCartDetails>
-
GetShoppingCartQueryStrategyHandler : IQueryStrategyHandler<GetShoppingCartQueryStrategy, ShoppingCartDetails>
-
GetShoppingCartDataQueryHandler : IDataQueryHandler<GetShoppingCartDataQuery, ShoppingCartDetails>
ApplicationDbContext
-
CreateShoppingCartDataCommandHandler : IDataCommandHandler<CreateShoppingCartDataCommand>
ApplicationDbContext
GetShoppingCartDataQueryHandler:IDataQueryHandler
ApplicationDbContext ,shoppingcartdetails>CreateShoppingCartDataCommandHandler:IDataCommandHandler
ApplicationDbContext -
GetShoppingCartQueryStrategyHandler:IQueryStrategyHandler
GetShoppingCartDataQueryHandler:IDataQueryHandler ,shoppingcartdetails>ApplicationDbContext CreateShoppingCartDataCommandHandler:IDataCommandHandler ,shoppingcartdetails>ApplicationDbContext -
UpdateShoppingCartDataCommandHandler : IDataCommandHandler<UpdateShoppingCartDataCommand>
-
SetItemPriceCommandStrategyHandler : ICommandStrategyHandler<SetItemPriceCommandStrategy>
-
GetProductDetailsDataQueryHandler : IDataQueryHandler<GetProductDetailsDataQuery, ProductDetails>
ApplicationDbContext
GetProductDetailsDataQueryHandler:IDataQueryHandler
ApplicationDbContext ,productdetails> -
-
SetTotalsCommandStrategyHandler : ICommandStrategyHandler<SetTotalsCommandStrategy>
-
SetDiscountsCommandStrategyHandler : ICommandStrategyHandler<SetDiscountsCommandStrategy>
?
SetSalesTaxCommandStrategyHandler : ICommandStrategyHandler<SetSalesTaxCommandStrategy>
SetDiscountsCommandStrategyHandler:ICommandStrategyHandler
? SetSalesTaxCommandStrategyHandler:ICommandStrategyHandler
-
GetShoppingCartDetailsQueryHandler:IQueryHandler
GetShoppingCartQueryStrategyHandler:IQueryStrategyHandler ,shoppingcartdetails>GetShoppingCartDataQueryHandler:IDataQueryHandler ,shoppingcartdetails>ApplicationDbContext CreateShoppingCartDataCommandHandler:IDataCommandHandler ,shoppingcartdetails>ApplicationDbContext UpdateShoppingCartDataCommandHandler:IDataCommandHandler
SetItemPriceCommandStrategyHandler:ICommandStrategyHandler
GetProductDetailsDataQueryHandler:IDataQueryHandler ApplicationDbContext ,productdetails>SetTotalsCommandStrategyHandler:ICommandStrategyHandler
SetDiscountsCommandStrategyHandler:ICommandStrategyHandler ? SetSalesTaxCommandStrategyHandler:ICommandStrategyHandler -
AddToCartCommandHandler:ICommandHandler
Implementation
DTOs
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://*.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; }
}
Calculating Order Totals
Rather than relying on a string of services to do simple (and required) arithmetic, I opted to put this behavior into extension methods so it is done on the fly against the actual data. Since this logic will need to be shared between the shopping cart, order, and quote, the calculation is done against IOrder
and IOrderItem
rather than concrete model types.
我没有依赖一串服务来进行简单(和必需)的算法,而是选择将此行为放入扩展方法中,以便它可以在实际数据中动态完成。由于此逻辑需要在购物车,订单和报价之间共享,因此计算是针对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
For brevity, we only show the AddToCart action, but this is where other actions against the shopping cart (i.e. remove from cart) would go as well.
为简洁起见,我们仅显示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
Here is where the main part of the workflow is executed. This command will be called directly from the AddToCart
controller action.
这是执行工作流的主要部分的位置。将直接从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
This updates the shopping cart with all of the changes that the business layer applied.
这会使用业务层应用的所有更改来更新购物车。
For the time being, this "command" does a query so it can reconcile the differences between the database and in memory copy. However, it is obviously a violation of the CQS pattern. I plan to make a follow-up question to determine what the best course of action is for change tracking since change tracking and CQS appear to be intimately linked.
目前,这个“命令”执行查询,以便它可以协调数据库和内存副本之间的差异。但是,这显然违反了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;
}
}
}
Do note that there is no shipping calculation in this workflow. This is primarily because the shipping calculation may depend on external APIs and it may take some time to return. Therefore, I am planning to make the AddToCart
workflow a step that runs instantaneously when an item is added and make a CalculateShippingAndTax
workflow that happens after the fact that updates the UI again after the totals have been retrieved from their (possibly external) sources, which might take time.
请注意,此工作流程中没有运费计算。这主要是因为运费计算可能依赖于外部API,并且可能需要一些时间才能返回。因此,我计划将AddToCart工作流程设置为在添加项目时立即运行的步骤,并使得在从其(可能是外部)源检索到总计之后再次更新UI的事实之后发生CalculateShippingAndTax工作流。可能需要时间。
Does this solve the problem? Yes, it does fix the real-world problems I was having when commands need to depend on queries.
这会解决问题吗?是的,它确实解决了当命令需要依赖查询时我遇到的实际问题。
However, it feels like this really only separates queries from commands conceptually. Physically, they still depend on one another unless you only look at the IDataCommand
and IDataQuery
abstractions that only depend on ApplicationDbContext
. I am not sure if this is the intent of qujck or not. I am also uncertain if this solves the bigger issue of the design being transferable to CQRS or not, but since it is not something I am planning for I am not that concerned about it.
但是,感觉这只是在概念上将查询与命令分开。在物理上,它们仍然相互依赖,除非您只查看仅依赖于ApplicationDbContext的IDataCommand和IDataQuery抽象。我不确定这是否是qujck的意图。我也不确定这是否解决了设计可以转移到CQRS的更大问题,但是因为它不是我计划的东西,所以我并不关心它。
#2
-1
There are always trade offs to consider between conflicting design principles. The way to resolve it is to look at the underlying reasons behind the principles. In this case, being unable to run a query without running the command is problematic, but being unable to run a command without running the query is generally harmless. As long as there's a way to run the query standalone, I see no reason not to add the query result to the command, especially if done something like this:
在冲突的设计原则之间总是要考虑权衡。解决问题的方法是查看原则背后的根本原因。在这种情况下,如果不运行命令而无法运行查询,则会出现问题,但如果不运行查询则无法运行命令通常是无害的。只要有一种方法可以独立运行查询,我认为没有理由不将查询结果添加到命令中,特别是如果完成这样的事情:
QueryResult command()
{
// do command stuff
return query();
}
#1
4
The answer to this problem came in the form of a comment by qujck.
这个问题的答案来自qujck的评论。
The solution is to break the application into different query types and command types. The exact purpose of each type remain a mystery (since the blog post doesn't go into the reasons why he made this distinction), but it does make it clear how top-level and mid-level commands can depend on database queries.
解决方案是将应用程序分解为不同的查询类型和命令类型。每种类型的确切目的仍然是一个谜(因为博客文章没有说明他之所以做出这种区分的原因),但它确实清楚地表明*和中级命令如何依赖于数据库查询。
Command Types
- Command (top-level)
- Command Strategy (mid-level)
- Data Command (direct data access)
指挥战略(中级)
数据命令(直接数据访问)
Query Types
- Query (top-level)
- Query Strategy (mid-level)
- Data Query (direct data access)
查询策略(中级)
数据查询(直接数据访问)
Command-Query Implementation
// 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 Dependency Graph
Using the above implementation, the structure of the AddToCart workflow dependency graph looks like this.
使用上面的实现,AddToCart工作流依赖关系图的结构如下所示。
-
AddToCartCommandHandler : ICommandHandler<AddToCartCommand>
-
GetShoppingCartDetailsQueryHandler : IQueryHandler<GetShoppingCartDetailsQuery, ShoppingCartDetails>
-
GetShoppingCartQueryStrategyHandler : IQueryStrategyHandler<GetShoppingCartQueryStrategy, ShoppingCartDetails>
-
GetShoppingCartDataQueryHandler : IDataQueryHandler<GetShoppingCartDataQuery, ShoppingCartDetails>
ApplicationDbContext
-
CreateShoppingCartDataCommandHandler : IDataCommandHandler<CreateShoppingCartDataCommand>
ApplicationDbContext
GetShoppingCartDataQueryHandler:IDataQueryHandler
ApplicationDbContext ,shoppingcartdetails>CreateShoppingCartDataCommandHandler:IDataCommandHandler
ApplicationDbContext -
GetShoppingCartQueryStrategyHandler:IQueryStrategyHandler
GetShoppingCartDataQueryHandler:IDataQueryHandler ,shoppingcartdetails>ApplicationDbContext CreateShoppingCartDataCommandHandler:IDataCommandHandler ,shoppingcartdetails>ApplicationDbContext -
UpdateShoppingCartDataCommandHandler : IDataCommandHandler<UpdateShoppingCartDataCommand>
-
SetItemPriceCommandStrategyHandler : ICommandStrategyHandler<SetItemPriceCommandStrategy>
-
GetProductDetailsDataQueryHandler : IDataQueryHandler<GetProductDetailsDataQuery, ProductDetails>
ApplicationDbContext
GetProductDetailsDataQueryHandler:IDataQueryHandler
ApplicationDbContext ,productdetails> -
-
SetTotalsCommandStrategyHandler : ICommandStrategyHandler<SetTotalsCommandStrategy>
-
SetDiscountsCommandStrategyHandler : ICommandStrategyHandler<SetDiscountsCommandStrategy>
?
SetSalesTaxCommandStrategyHandler : ICommandStrategyHandler<SetSalesTaxCommandStrategy>
SetDiscountsCommandStrategyHandler:ICommandStrategyHandler
? SetSalesTaxCommandStrategyHandler:ICommandStrategyHandler
-
GetShoppingCartDetailsQueryHandler:IQueryHandler
GetShoppingCartQueryStrategyHandler:IQueryStrategyHandler ,shoppingcartdetails>GetShoppingCartDataQueryHandler:IDataQueryHandler ,shoppingcartdetails>ApplicationDbContext CreateShoppingCartDataCommandHandler:IDataCommandHandler ,shoppingcartdetails>ApplicationDbContext UpdateShoppingCartDataCommandHandler:IDataCommandHandler
SetItemPriceCommandStrategyHandler:ICommandStrategyHandler
GetProductDetailsDataQueryHandler:IDataQueryHandler ApplicationDbContext ,productdetails>SetTotalsCommandStrategyHandler:ICommandStrategyHandler
SetDiscountsCommandStrategyHandler:ICommandStrategyHandler ? SetSalesTaxCommandStrategyHandler:ICommandStrategyHandler -
AddToCartCommandHandler:ICommandHandler
Implementation
DTOs
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://*.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; }
}
Calculating Order Totals
Rather than relying on a string of services to do simple (and required) arithmetic, I opted to put this behavior into extension methods so it is done on the fly against the actual data. Since this logic will need to be shared between the shopping cart, order, and quote, the calculation is done against IOrder
and IOrderItem
rather than concrete model types.
我没有依赖一串服务来进行简单(和必需)的算法,而是选择将此行为放入扩展方法中,以便它可以在实际数据中动态完成。由于此逻辑需要在购物车,订单和报价之间共享,因此计算是针对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
For brevity, we only show the AddToCart action, but this is where other actions against the shopping cart (i.e. remove from cart) would go as well.
为简洁起见,我们仅显示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
Here is where the main part of the workflow is executed. This command will be called directly from the AddToCart
controller action.
这是执行工作流的主要部分的位置。将直接从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
This updates the shopping cart with all of the changes that the business layer applied.
这会使用业务层应用的所有更改来更新购物车。
For the time being, this "command" does a query so it can reconcile the differences between the database and in memory copy. However, it is obviously a violation of the CQS pattern. I plan to make a follow-up question to determine what the best course of action is for change tracking since change tracking and CQS appear to be intimately linked.
目前,这个“命令”执行查询,以便它可以协调数据库和内存副本之间的差异。但是,这显然违反了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;
}
}
}
Do note that there is no shipping calculation in this workflow. This is primarily because the shipping calculation may depend on external APIs and it may take some time to return. Therefore, I am planning to make the AddToCart
workflow a step that runs instantaneously when an item is added and make a CalculateShippingAndTax
workflow that happens after the fact that updates the UI again after the totals have been retrieved from their (possibly external) sources, which might take time.
请注意,此工作流程中没有运费计算。这主要是因为运费计算可能依赖于外部API,并且可能需要一些时间才能返回。因此,我计划将AddToCart工作流程设置为在添加项目时立即运行的步骤,并使得在从其(可能是外部)源检索到总计之后再次更新UI的事实之后发生CalculateShippingAndTax工作流。可能需要时间。
Does this solve the problem? Yes, it does fix the real-world problems I was having when commands need to depend on queries.
这会解决问题吗?是的,它确实解决了当命令需要依赖查询时我遇到的实际问题。
However, it feels like this really only separates queries from commands conceptually. Physically, they still depend on one another unless you only look at the IDataCommand
and IDataQuery
abstractions that only depend on ApplicationDbContext
. I am not sure if this is the intent of qujck or not. I am also uncertain if this solves the bigger issue of the design being transferable to CQRS or not, but since it is not something I am planning for I am not that concerned about it.
但是,感觉这只是在概念上将查询与命令分开。在物理上,它们仍然相互依赖,除非您只查看仅依赖于ApplicationDbContext的IDataCommand和IDataQuery抽象。我不确定这是否是qujck的意图。我也不确定这是否解决了设计可以转移到CQRS的更大问题,但是因为它不是我计划的东西,所以我并不关心它。
#2
-1
There are always trade offs to consider between conflicting design principles. The way to resolve it is to look at the underlying reasons behind the principles. In this case, being unable to run a query without running the command is problematic, but being unable to run a command without running the query is generally harmless. As long as there's a way to run the query standalone, I see no reason not to add the query result to the command, especially if done something like this:
在冲突的设计原则之间总是要考虑权衡。解决问题的方法是查看原则背后的根本原因。在这种情况下,如果不运行命令而无法运行查询,则会出现问题,但如果不运行查询则无法运行命令通常是无害的。只要有一种方法可以独立运行查询,我认为没有理由不将查询结果添加到命令中,特别是如果完成这样的事情:
QueryResult command()
{
// do command stuff
return query();
}