Nig*_*888 6 asp.net-mvc design-patterns entity-framework command-query-separation
给定一个真实的匿名购物车,"AddToCart"工作流程必须执行以下步骤:
因此,虽然"AddToCart"听起来应该是一个命令(因为它更新了系统状态),但实际上它依赖于许多查询.
处理这样的工作流程的普遍接受的方法是什么?
AddToCartCommandHandler取决于可能运行查询的其他服务.CartService,用于协调运行查询的工作流,然后是命令.我之所以无法找到答案,是因为它"取决于设计",这是不适用的例外情况之一吗?
如果命令和查询是分开的,我是否会将我的真实实体框架实体类传递给添加/更新购物车的命令(因此EF可以确定它是否附加)?在这种情况下,似乎DTO不会这样做.
注意:我隐含地假设实现的系统这样
CQS做的目的是最终它们可以成为一个完整的CQRS系统.如果是这样,这个工作流程显然无法进行转换 - 因此我的问题.
我正在第一次尝试CQS.
从我阅读的有关此模式的文档中可以清楚地看出,查询不得更改系统状态.
但是,目前还不清楚是否可以从命令中运行查询(我似乎无法在任何地方找到任何信息).
我可以想到几个真实案例需要发生的事情.但是,鉴于在线缺乏这种模式的现实例子,我不确定如何继续.在线有很多理论,但我能找到的唯一代码就是这里和这里.
这个问题的答案以qujck 的评论的形式出现。
解决方案是将应用程序分为不同的查询类型和命令类型。每种类型的确切目的仍然是个谜(因为博客文章没有详细说明他做出这种区分的原因),但它确实清楚地表明了顶级和中级命令如何依赖于数据库查询。
// Commands
public interface ICommand
{
}
public interface IDataCommand
{
}
/// <summary>
/// A holistic abstraction, an abstraction that acts as the whole of each transaction
/// </summary>
/// <typeparam name="TCommand"></typeparam>
public interface ICommandHandler<TCommand>
{
void Handle(TCommand command);
}
public interface ICommandStrategyHandler<TCommand> where TCommand : ICommand
{
void Handle(TCommand command);
}
/// <summary>
/// Direct database update
/// </summary>
/// <typeparam name="TCommand"></typeparam>
public interface IDataCommandHandler<TCommand> where TCommand : IDataCommand
{
void Handle(TCommand command);
}
// Queries
public interface IQuery<TResult>
{
}
public interface IDataQuery<TResult>
{
}
/// <summary>
/// A holistic abstraction, an abstraction that acts as the whole of each transaction
/// </summary>
/// <typeparam name="TQuery"></typeparam>
/// <typeparam name="TResult"></typeparam>
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
TResult Handle(TQuery query);
}
public interface IQueryStrategyHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
TResult Handle(TQuery query);
}
/// <summary>
/// Direct database query
/// </summary>
/// <typeparam name="TQuery"></typeparam>
/// <typeparam name="TResult"></typeparam>
public interface IDataQueryHandler<TQuery, TResult> where TQuery : IDataQuery<TResult>
{
TResult Handle(TQuery query);
}
/// <summary>
/// Generic processor that can run any query
/// </summary>
public interface IQueryProcessor
{
TResult Execute<TResult>(IQuery<TResult> query);
// NOTE: Stephen recommends against using Async. He may be right that it is not
// worth the aggrevation of bugs that may be introduced.
//Task<TResult> Execute<TResult>(IQuery<TResult> query);
TResult Execute<TResult>(IDataQuery<TResult> query);
}
Run Code Online (Sandbox Code Playgroud)
使用上述实现,AddToCart 工作流依赖关系图的结构如下所示。
AddToCartCommandHandler : ICommandHandler<AddToCartCommand>
GetShoppingCartDetailsQueryHandler : IQueryHandler<GetShoppingCartDetailsQuery, ShoppingCartDetails>
GetShoppingCartQueryStrategyHandler : IQueryStrategyHandler<GetShoppingCartQueryStrategy, ShoppingCartDetails>
GetShoppingCartDataQueryHandler : IDataQueryHandler<GetShoppingCartDataQuery, ShoppingCartDetails>
ApplicationDbContextCreateShoppingCartDataCommandHandler : IDataCommandHandler<CreateShoppingCartDataCommand>
ApplicationDbContextUpdateShoppingCartDataCommandHandler : IDataCommandHandler<UpdateShoppingCartDataCommand>SetItemPriceCommandStrategyHandler : ICommandStrategyHandler<SetItemPriceCommandStrategy>
GetProductDetailsDataQueryHandler : IDataQueryHandler<GetProductDetailsDataQuery, ProductDetails>
ApplicationDbContextSetTotalsCommandStrategyHandler : ICommandStrategyHandler<SetTotalsCommandStrategy>
SetDiscountsCommandStrategyHandler : ICommandStrategyHandler<SetDiscountsCommandStrategy>
?SetSalesTaxCommandStrategyHandler : ICommandStrategyHandler<SetSalesTaxCommandStrategy>public class ShoppingCartDetails : IOrder
{
private IEnumerable<IOrderItem> items = new List<ShoppingCartItem>();
public Guid Id { get; set; }
public decimal SubtotalDiscounts { get; set; }
public string ShippingPostalCode { get; set; }
public decimal Shipping { get; set; }
public decimal ShippingDiscounts { get; set; }
public decimal SalesTax { get; set; }
public decimal SalesTaxDiscounts { get; set; }
// Declared twice - once for the IOrder interface
// and once so we can get the realized concrete type.
// See: /sf/ask/1084344341/
public IEnumerable<ShoppingCartItem> Items
{
get { return this.items as IEnumerable<ShoppingCartItem>; }
set { this.items = value; }
}
IEnumerable<IOrderItem> IOrder.Items
{
get { return this.items; }
set { this.items = value; }
}
//public IEnumerable<ShoppingCartNotification> Notifications { get; set; }
//public IEnumerable<ShoppingCartCoupon> Coupons { get; set; } // TODO: Add this to IOrder
}
public class ShoppingCartItem : IOrderItem
{
public ShoppingCartItem()
{
this.Id = Guid.NewGuid();
this.Selections = new Dictionary<string, object>();
}
public Guid Id { get; set; }
public Guid ShoppingCartId { get; set; }
public Guid ProductId { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public decimal PriceDiscount { get; set; }
public IDictionary<string, object> Selections { get; set; }
}
public class ProductDetails
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public decimal Discount { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
我没有依赖一系列服务来执行简单(且必需)的算术,而是选择将此行为放入扩展方法中,以便根据实际数据即时完成。由于此逻辑需要在购物车、订单和报价之间共享,因此计算是针对IOrder而IOrderItem不是具体模型类型完成的。
// Contract to share simple cacluation and other business logic between shopping cart, order, and quote
public interface IOrder
{
decimal SubtotalDiscounts { get; set; }
decimal Shipping { get; set; }
decimal ShippingDiscounts { get; set; }
decimal SalesTax { get; set; }
decimal SalesTaxDiscounts { get; set; }
IEnumerable<IOrderItem> Items { get; set; }
}
public interface IOrderItem
{
Guid ProductId { get; set; }
int Quantity { get; set; }
decimal Price { get; set; }
decimal PriceDiscount { get; set; }
IDictionary<string, object> Selections { get; set; }
}
public static class OrderExtensions
{
public static decimal GetSubtotal(this IOrder order)
{
return order.Items.Sum(x => x.GetTotal());
}
public static decimal GetSubtotalBeforeDiscounts(this IOrder order)
{
return order.Items.Sum(x => x.GetTotalBeforeDiscounts());
}
public static decimal GetTotal(this IOrder order)
{
var subtotal = (order.GetSubtotal() - order.SubtotalDiscounts);
var shipping = (order.Shipping - order.ShippingDiscounts);
var salesTax = (order.SalesTax - order.SalesTaxDiscounts);
return (subtotal + shipping + salesTax);
}
}
public static class OrderItemExtensions
{
public static decimal GetTotalBeforeDiscounts(this IOrderItem item)
{
return (item.Price * item.Quantity);
}
public static decimal GetTotal(this IOrderItem item)
{
return (GetTotalBeforeDiscounts(item) - item.PriceDiscount);
}
public static decimal GetDiscountedUnitPrice(this IOrderItem item)
{
return (item.Quantity > 0) ? (GetTotal(item) / item.Quantity) : 0;
}
}
Run Code Online (Sandbox Code Playgroud)
为简洁起见,我们仅显示 AddToCart 操作,但这也是针对购物车的其他操作(即从购物车中删除)的地方。
public class ShoppingCartController : Controller
{
private readonly IQueryProcessor queryProcessor;
private readonly IAnonymousIdAccessor anonymousIdAccessor;
private readonly ICommandHandler<AddToCartCommand> addToCartHandler;
public ShoppingCartController(
IQueryProcessor queryProcessor,
IAnonymousIdAccessor anonymousIdAccessor,
ICommandHandler<AddToCartCommand> addToCartHandler)
{
if (queryProcessor == null)
throw new ArgumentNullException("queryProcessor");
if (anonymousIdAccessor == null)
throw new ArgumentNullException("anonymousIdAccessor");
if (addToCartHandler == null)
throw new ArgumentNullException("addToCartHandler");
this.queryProcessor = queryProcessor;
this.anonymousIdAccessor = anonymousIdAccessor;
this.addToCartHandler = addToCartHandler;
}
public ActionResult Index()
{
var command = new GetShoppingCartDetailsQuery
{
ShoppingCartId = this.anonymousIdAccessor.AnonymousID
};
ShoppingCartDetails cart = this.queryProcessor.Execute(command);
return View(cart);
}
public ActionResult AddToCart(ItemViewModel model)
{
var command = new AddToCartCommand
{
ProductId = model.Id,
Quantity = model.Qty,
Selections = model.Selections,
ShoppingCartId = this.anonymousIdAccessor.AnonymousID
};
this.addToCartHandler.Handle(command);
// If we execute server side, it should go to the cart page
return RedirectToAction("Index");
}
}
Run Code Online (Sandbox Code Playgroud)
这是工作流程的主要部分执行的地方。该命令将直接从控制器操作调用AddToCart。
public class AddToCartCommandHandler : ICommandHandler<AddToCartCommand>
{
private readonly IQueryStrategyHandler<GetShoppingCartQueryStrategy, ShoppingCartDetails> getShoppingCartQuery;
private readonly IDataCommandHandler<UpdateShoppingCartDataCommand> updateShoppingCartCommand;
private readonly ICommandStrategyHandler<SetItemPriceCommandStrategy> setItemPriceCommand;
private readonly ICommandStrategyHandler<SetTotalsCommandStrategy> setTotalsCommand;
public AddToCartCommandHandler(
IQueryStrategyHandler<GetShoppingCartQueryStrategy, ShoppingCartDetails> getShoppingCartCommand,
IDataCommandHandler<UpdateShoppingCartDataCommand> updateShoppingCartCommand,
ICommandStrategyHandler<SetItemPriceCommandStrategy> setItemPriceCommand,
ICommandStrategyHandler<SetTotalsCommandStrategy> setTotalsCommand
)
{
if (getShoppingCartCommand == null)
throw new ArgumentNullException("getShoppingCartCommand");
if (setItemPriceCommand == null)
throw new ArgumentNullException("setItemPriceCommand");
if (updateShoppingCartCommand == null)
throw new ArgumentNullException("updateShoppingCartCommand");
if (setTotalsCommand == null)
throw new ArgumentNullException("setTotalsCommand");
this.getShoppingCartQuery = getShoppingCartCommand;
this.updateShoppingCartCommand = updateShoppingCartCommand;
this.setItemPriceCommand = setItemPriceCommand;
this.setTotalsCommand = setTotalsCommand;
}
public void Handle(AddToCartCommand command)
{
// Get the shopping cart (aggregate root) from the database
var shoppingCart = getShoppingCartQuery.Handle(new GetShoppingCartQueryStrategy { ShoppingCartId = command.ShoppingCartId });
// Create a new shopping cart item
var item = new Contract.DTOs.ShoppingCartItem
{
ShoppingCartId = command.ShoppingCartId,
ProductId = command.ProductId,
Quantity = command.Quantity,
// Dictionary representing the option selections the user made on the UI
Selections = command.Selections
};
// Set the item's price (calculated/retrieved from database query)
setItemPriceCommand.Handle(new SetItemPriceCommandStrategy { ShoppingCartItem = item });
// Add the item to the cart
var items = new List<Contract.DTOs.ShoppingCartItem>(shoppingCart.Items);
items.Add(item);
shoppingCart.Items = items;
// Set the shopping cart totals (sales tax, discounts)
setTotalsCommand.Handle(new SetTotalsCommandStrategy { ShoppingCart = shoppingCart });
// Update the shopping cart details in the database
updateShoppingCartCommand.Handle(new UpdateShoppingCartDataCommand { ShoppingCart = shoppingCart });
}
}
Run Code Online (Sandbox Code Playgroud)
public class GetShoppingCartQueryStrategyHandler : IQueryStrategyHandler<GetShoppingCartQueryStrategy, ShoppingCartDetails>
{
private readonly IDataQueryHandler<GetShoppingCartDataQuery, ShoppingCartDetails> getShoppingCartDataQuery;
private readonly IDataCommandHandler<CreateShoppingCartDataCommand> createShoppingCartDataCommand;
public GetShoppingCartQueryStrategyHandler(
IDataQueryHandler<GetShoppingCartDataQuery, ShoppingCartDetails> getShoppingCartDataQuery,
IDataCommandHandler<CreateShoppingCartDataCommand> createShoppingCartDataCommand)
{
if (getShoppingCartDataQuery == null)
throw new ArgumentNullException("getShoppingCartDataQuery");
if (createShoppingCartDataCommand == null)
throw new ArgumentNullException("createShoppingCartDataCommand");
this.getShoppingCartDataQuery = getShoppingCartDataQuery;
this.createShoppingCartDataCommand = createShoppingCartDataCommand;
}
public ShoppingCartDetails Handle(GetShoppingCartQueryStrategy query)
{
var result = this.getShoppingCartDataQuery.Handle(new GetShoppingCartDataQuery { ShoppingCartId = query.ShoppingCartId });
// If there is no shopping cart, create one.
if (result == null)
{
this.createShoppingCartDataCommand.Handle(new CreateShoppingCartDataCommand { ShoppingCartId = query.ShoppingCartId });
result = new ShoppingCartDetails
{
Id = query.ShoppingCartId
};
}
return result;
}
}
Run Code Online (Sandbox Code Playgroud)
/// <summary>
/// Data handler to get the shopping cart data (if it exists)
/// </summary>
public class GetShoppingCartDataQueryHandler : IDataQueryHandler<GetShoppingCartDataQuery, ShoppingCartDetails>
{
private readonly IAppContext context;
public GetShoppingCartDataQueryHandler(IAppContext context)
{
if (context == null)
throw new ArgumentNullException("context");
this.context = context;
}
public ShoppingCartDetails Handle(GetShoppingCartDataQuery query)
{
return (from shoppingCart in context.ShoppingCarts
where shoppingCart.Id == query.ShoppingCartId
select new ShoppingCartDetails
{
Id = shoppingCart.Id,
SubtotalDiscounts = shoppingCart.SubtotalDiscounts,
ShippingPostalCode = shoppingCart.ShippingPostalCode,
Shipping = shoppingCart.Shipping,
ShippingDiscounts = shoppingCart.ShippingDiscounts,
SalesTax = shoppingCart.SalesTax,
SalesTaxDiscounts = shoppingCart.SalesTaxDiscounts,
Items = shoppingCart.Items.Select(i =>
new Contract.DTOs.ShoppingCartItem
{
Id = i.Id,
ShoppingCartId = i.ShoppingCartId,
ProductId = i.ProductId,
Quantity = i.Quantity,
Price = i.Price,
PriceDiscount = i.PriceDiscount
// TODO: Selections...
})
}).FirstOrDefault();
}
}
Run Code Online (Sandbox Code Playgroud)
public class CreateShoppingCartDataCommandHandler : IDataCommandHandler<CreateShoppingCartDataCommand>
{
private readonly IAppContext context;
public CreateShoppingCartDataCommandHandler(IAppContext context)
{
if (context == null)
throw new ArgumentNullException("context");
this.context = context;
}
public void Handle(CreateShoppingCartDataCommand command)
{
var cart = new ShoppingCart
{
Id = command.ShoppingCartId
};
this.context.ShoppingCarts.Add(cart);
this.context.SaveChanges();
}
}
Run Code Online (Sandbox Code Playgroud)
这将使用业务层应用的所有更改更新购物车。
目前,这个“命令”执行查询,以便它可以协调数据库和内存副本之间的差异。然而,这显然违反了CQS模式。我计划提出一个后续问题,以确定变更跟踪的最佳行动方案是什么,因为变更跟踪和 CQS 似乎密切相关。
public class UpdateShoppingCartDataCommandHandler : IDataCommandHandler<UpdateShoppingCartDataCommand>
{
private readonly IAppContext context;
public UpdateShoppingCartDataCommandHandler(IAppContext context)
{
if (context == null)
throw new ArgumentNullException("context");
this.context = context;
}
public void Handle(UpdateShoppingCartDataCommand command)
{
var cart = context.ShoppingCarts
.Include(x => x.Items)
.Single(x => x.Id == command.ShoppingCart.Id);
cart.Id = command.ShoppingCart.Id;
cart.SubtotalDiscounts = command.ShoppingCart.SubtotalDiscounts;
cart.ShippingPostalCode = command.ShoppingCart.ShippingPostalCode;
cart.Shipping = command.ShoppingCart.Shipping;
cart.ShippingDiscounts = command.ShoppingCart.ShippingDiscounts;
cart.SalesTax = command.ShoppingCart.SalesTax;
cart.SalesTaxDiscounts = command.ShoppingCart.SalesTaxDiscounts;
ReconcileShoppingCartItems(cart.Items, command.ShoppingCart.Items, command.ShoppingCart.Id);
// Update the cart with new data
context.SaveChanges();
}
private void ReconcileShoppingCartItems(ICollection<ShoppingCartItem> items, IEnumerable<Contract.DTOs.ShoppingCartItem> itemDtos, Guid shoppingCartId)
{
// remove deleted items
var items2 = new List<ShoppingCartItem>(items);
foreach (var item in items2)
{
if (!itemDtos.Any(x => x.Id == item.Id))
{
context.Entry(item).State = EntityState.Deleted;
}
}
// Add/update items
foreach (var dto in itemDtos)
{
var item = items.FirstOrDefault(x => x.Id == dto.Id);
if (item == null)
{
items.Add(new ShoppingCartItem
{
Id = Guid.NewGuid(),
ShoppingCartId = shoppingCartId,
ProductId = dto.ProductId,
Quantity = dto.Quantity,
Price = dto.Price,
PriceDiscount = dto.PriceDiscount
});
}
else
{
item.ProductId = dto.ProductId;
item.Quantity = dto.Quantity;
item.Price = dto.Price;
item.PriceDiscount = dto.PriceDiscount;
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
public class SetItemPriceCommandStrategyHandler : ICommandStrategyHandler<SetItemPriceCommandStrategy>
{
private readonly IDataQueryHand
| 归档时间: |
|
| 查看次数: |
459 次 |
| 最近记录: |