I have Customer
, Order
, and Product
Aggregate Roots… When the order is created, it takes in a Customer
and a List<Sales.Items>
. It looks something like:
public class Order
{
public static Order Create(Customer customer, List<Sales.Item> items)
{
// Creates the order
return newOrder;
}
}
Then using CQRS I've created OrderCreateHandler
which looks something like:
public class OrderCreateCommandHandler : IRequestHandler<OrderCreateCommand>
{
public OrderCreateCommandHandler(ECommerceContext db)
{
_db = db;
}
public async Task<Unit> Handle(OrderCreateCommand request, CancellationToken cancellationToken)
{
var customerResult = // Q) Is it okay to execute a CustomerQuery here?
var customer = new Customer(customerResult.Id, customerResult.FirstName, customerResult.MiddleName,
customerResult.LastName, customerResult.StreetAddress, customerResult.City, customerResult.State, "United States",
customerResult.ZipCode);
// blah....
order = Order.Create(customer, products);
_db.Orders.Add(order);
}
}
My question is in the command handler, is it okay to perform queries to get the data to build the aggregate roots I need to then pass? I don't ever store the aggregate root itself (just a reference if I need), but I don't want to pass in Ids and primitives everywhere, I want to follow SOLID OO and pass actual objects. Is this a violation of CQRS?
Best Answer
Let's start with a short review of the problem-space here. The fundamental benefit of adopting a CQRS pattern is to solve/simplify your problem domain by reducing the interleaving and leakage that begins to occur when utilizing the same model for your write-side as your read-side. Often, the tension that arises serves as a detriment to both. CQRS seeks to relieve this tension by separating (decoupling logically and possibly physically) the write-side and read-side of your system. With the above in mind it should be clear that neither your commands nor queries should be coupled to a logical entity from the other "side".
Given the above, we can now formulate a direct answer to your question: Yes, you can query your data store within a command handler provided the query is issued against your command model. Because your
OrderCreateCommandHandler
is part of the command model of your application, we want to avoid coupling it to any part of your read model. It's unclear whether or not this is case given the example code (although the nameCustomerQuery
does raise some suspicions).More important than the answer above though is that... there is something else that feels fishy about the example you have provided. Can you feel that too?
What I see here is quite a bit of coupling. Your handler is retrieving a
CustomerResult
(VO?), then breaking down all of it's data into another entity's constructor (Customer
), then passing theCustomer
to a factory method of yet another entity. We have quite a bit of "asking" happening here. That is, we are passing around a lot of data in way that creates coupling.Furthermore, the command handler doesn't "read" in a very declarative fashion (which is what we want to strive for). What I mean is that it's kind of hard to "see" what's happening in your method because there is so much plumbing getting in the way. I think we can come up with a more cohesive/declarative solution.
Given that the general "flow" of a command handler can be broken down into three simple steps:
Let us see if we can come up with a simpler solution:
Ah ha! Much cleaner (3 simple steps). More importantly though, not only does the code above achieve your same goal, it does so without creating many dependencies between disparate objects as wells as functioning in a more declarative and encapsulated manner (we aren't "newing" anything or calling any factory methods)! Let's break this down piece by piece so we can understand "why" the above may be a better solution.
buyer = buyers.Find( cmd.CustomerId );
The first thing I've done is introduce a new concept:
Buyer
. In so doing this, I am partitioning your data vertically according to behavior. Let's let yourCustomer
entity have responsibility for maintainingCustomer
information (FirstName
,LastName
,Email
, etc.), and allow aBuyer
to be responsible for making purchases. Because someCustomer
information needs to be recorded when a purchase is made, we will hydrate aBuyer
with a "snapshot" of that data (and possibly other data).buyer.PlaceOrder( cmd.Products );
Next we coordinate the purchase. The above method is where a
new Order
is created. AnOrder
doesn't just appear out of nowhere right? Something must place it, so we model accordingly. What does this achieve? Well, theBuyer.PlaceOrder
method provides a place in your domain to throwBuyerNotInGoodStanding
,OrderExceedsBuyerSpendingLimit
, orRepeatOrderDetected
exceptions. By only creating anOrder
in the context of it's placement, we can enforce how anOrder
can come about. In your example, either your application-layer command handler or yourOrder
factory method would have to be made responsible for enforcing each invariant. Neither is a good place for checking business rules. Additionally we now have a place to raise ourOrderPlaced
event (which will be necessary to keep your payment context decoupled), and also we can simplify yourOrder
entity as it now only needs a scalarbuyerId
to keep reference to it's owner.buyers.Save( buyer );
Pretty self-explanatory. A
Buyer
now contains all of the information you need to persist both anOrder
and a "snapshot" ofCustomer
data. How you organize that data internally and take it apart for persistence is up to you (hint: ABuyer
needn't be persisted at all, for example. Just theOrder
it contains).EDIT
The example solution (if we can call it that) that I posted is one meant to get the "gears turning", and doesn't necessarily represent the best-possible solution to the problem at hand. That is, your problem. It is totally possible (even likely) that introducing the concept of a
Buyer
aggregate is over-engineering given that there had been no mention of any sort of rules regarding how anOrder
can be placed. For example:may be a totally valid approach! Just be sure to include all of the necessary information in the
CustomerInformationSlip
(your "snapshot") attached to theOrder
to allow it to enforce any invariant controlling how it can be modified. For example:The above may throw an
OrderExceedsCustomerShippingDistance
if eachCustomer
has their own rules regarding how far you will ship to them given their account tier.Let the rules dictate the design!