Given a real-world anonymous shopping cart, the "AddToCart" workflow must do the following steps:
- 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.
My Question
What is the generally accepted way to handle workflows like this?
- Make an
AddToCartCommandHandlerthat depends on other services that may run queries. - Make a facade
CartServicethat 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?
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.
NOTE: I am implicitly assuming that systems that implement
CQSdo so with the aim that eventually they could become a full-onCQRSsystem. If so, this workflow apparently would not be able to make the transition - hence my question.
Background
I am taking my first stab at 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.
The answer to this problem came in the form of a comment by 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
Query Types
Command-Query Implementation
AddToCart Dependency Graph
Using the above implementation, the structure of the AddToCart workflow dependency graph looks like this.
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>Implementation
DTOs
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
IOrderandIOrderItemrather than concrete model types.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.
AddToCartCommandHandler
Here is where the main part of the workflow is executed. This command will be called directly from the
AddToCartcontroller action.GetShoppingCartQueryStrategyHandler
GetShoppingCartDataQueryHandler
CreateShoppingCartDataCommandHandler
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.
SetItemPriceCommandStrategyHandler
GetProductDetailsDataQueryHandler
SetTotalsCommandStrategyHandler
SetDiscountsCommandStrategyHandler
SetSalesTaxCommandStrategyHandler
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
AddToCartworkflow a step that runs instantaneously when an item is added and make aCalculateShippingAndTaxworkflow 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.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
IDataCommandandIDataQueryabstractions that only depend onApplicationDbContext. 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.