How to write commands for an object that is an aggregating root in one context, but not in another?

I am working on a project for a company that believes that suppliers perform services to redistribute employees. These services mean that the driving forces do not have experience, for example, for preparing the piano or for transit or creating boxes for valuables.

In this domain, the order has 1: many places.

In a moving industry, orders often go backwards until the supplier completes the services requested. Therefore, in our model, we have some statuses (for example, sent, canceled, in standby mode) that apply to orders and locations.

There are pretty simple business rules here. Here's a sample:

  • When an order is placed on hold, all locations are placed on hold.
  • A location cannot be withdrawn Hold, if its parent order is on hold.

Etc. From these rules, it seems obvious to me that this forms the cumulative border of the root. So I have a MyClient.Statuses.Order aggregate, where Statuses is the name of the context / service / of what you want to name:

 public class Order { private Guid _id; private OrderStatus _status; public void PlaceOnHold() { if (_status == OrderStatus.Cancelled) // throw exception _status = OrderStatus.OnHold; Locations.ForEach(loc => loc.PlaceOnHold()); } public void PlaceLocationOnHold(Guid id) { if (_status == OrderStatus.Cancelled) // throw exception Locations.Single(loc => loc.Id == id).PlaceOnHold(); } // etc... private Location[] Locations; } internal class Location { public Guid Id; public LocationStatus Status; public void PlaceOnHold() { // It ok for a cancelled location on a non-cancelled order, // but a Location cannot be placed On Hold if it Cancelled so // just ignore it if (Status == LocationStatus.Cancelled) return; Status = LocationStatus.OnHold; } } 

Both of these objects (Order, Location) have GUIDs in other contexts (for example, for CRUD-based attributes that have no states). So, now we finally come to my question:

How to write a command and a handler to place a place on hold?

I want to keep this DRY and Service-Oriented thing in order to minimize communication, but it is very difficult to maintain the relationship between parents and children between two objects in only one place.

Option 1 - Single Location ID:

 public class PlaceLocationOnHold_V1 { public readonly Guid Id; } public class PlaceLocationOnHold_V1Handler { public void Handle(PlaceLocationOnHold_V1 command) { // This is typically a no-no. Should only fetch by OrderId: var aggregate = _repository.GetByLocationId(command.Id); aggregate.PlaceLocationOnHold(command.Id); _repository.Save(); } } 

Option 2 - Order ID and Location ID:

 public class PlaceLocationOnHold_V2 { public readonly Guid OrderId; // This feels redundant public readonly Guid LocationId; } public class PlaceLocationOnHold_V2Handler { public void Handle(PlaceLocationOnHold_V2 command) { var aggregate = _repository.GetById(command.OrderId); aggregate.PlaceLocationOnHold(command.LocationId); _repository.Save(); } } 

Option 3 is the only parameter with a class that encapsulates "A Location owned by order"

 public class LocationIdentity { public Guid Id; public Guid OrderId; } public class PlaceLocationOnHold_V3 { public readonly LocationIdentity Location; } public class PlaceLocationOnHold_V3Handler { public void Handle(PlaceLocationOnHold_V3 command) { var aggregate = _repository.GetById(command.Location.OrderId); aggregate.PlaceLocationOnHold(command.Location.Id); _repository.Save(); } } 
+4
source share
1 answer

Check out Vaughn Vernon's articles on Effective Aggregate Design . Part 2 in particular - there is some good information about modeling aggregates that communicate with each other.

The main problem that is missing from your design is that these are both ARs, as you already mentioned - they are globally identified. Therefore, they must refer to each other by ID. An order must not contain a child collection of places.

That way, your Order class will have a LocationIds collection, and your location will have OrderId.

 public class Order { private Guid _id; private OrderStatus _status; private Guid[] _locationIds; //... } public class Location { private Guid _id; private Guid _orderId; //... } 

Once you figured it out right, option number 1 makes sense. Since Location is AR for yourself, you can instantiate it and call PlaceOnHold directly on it without executing the AR order.

As for the situation where a change in one AR flows down to another (i.e. placing a hold order also forces all locations to be held as well), you can use domain events or possible consistency.

 public class Order { //... private instance variables public void PlaceOnHold() { if (_status == OrderStatus.Cancelled) // throw exception _status == Orderstatus.OnHold; DomainEvents.Handle(new OrderPlacedOnHold(_id)); // handle this, look up the related locations and call PlaceOnHold on each of them) } } 

And for a situation where you can try to remove the location binding, but the order is suspended, which makes the action illegal, you can create an instance of the Order object in the command handler and pass it to the RemoveFromHold method of Location. Vernon mentions this and reiterates the fact that just because you can only use one AR for each transaction does not mean that you cannot create multiple AR transactions in a transaction.

 public class RemoveHoldFromLocation : IHandler<RemoveHoldFromLocationCommand> { public void Execute(RemoveHoldFromLocationCommand cmd) { var location = locationRepo.Get(cmd.LocationId); var order = orderRepo.Get(location.GetOrderId()); location.RemoveHold(order.GetStatus()); } } public class Location { //... private instance variables, etc. public void RemoveHold(OrderStatus orderStatus) { if (orderStatus == OrderStatus.OnHold) // throw Exception _status == LocationStatus.OnHold; } } 

This is just pseudo code, so forgive typos, etc. Similar code samples are found in Vernon PDF.

+8
source

All Articles