Being one of the participants in the discussion mentioned above, I can share some insight. If you look at a project like NCQRS which formalized how entities are constructed and hydrated in a rather explicit way, you'll notice there is a certain rigidity that comes with that approach. For entities I found that I evolve their storage as state in the aggregate from a simple field, list or dictionary to a dedicated collection class depending on how scattered behavior is becoming to maintain them. Less rigidity brings freedom, choice of modeling within the aggregate boundary. I value that a lot.
The routing of events upon rehydration happens on the inside of the aggregate. It's not something that should be externalized IMO. There are various ways to go about it. In my own project this is how I formalized it in a very lightweight fashion (only entity shown here):
/// <summary>
/// Base class for aggregate entities that need some basic infrastructure for tracking state changes on their aggregate root entity.
/// </summary>
public abstract class Entity : IInstanceEventRouter
{
readonly Action<object> _applier;
readonly InstanceEventRouter _router;
/// <summary>
/// Initializes a new instance of the <see cref="Entity"/> class.
/// </summary>
/// <param name="applier">The event player and recorder.</param>
/// <exception cref="System.ArgumentNullException">Thrown when the <paramref name="applier"/> is null.</exception>
protected Entity(Action<object> applier)
{
if (applier == null) throw new ArgumentNullException("applier");
_applier = applier;
_router = new InstanceEventRouter();
}
/// <summary>
/// Registers the state handler to be invoked when the specified event is applied.
/// </summary>
/// <typeparam name="TEvent">The type of the event to register the handler for.</typeparam>
/// <param name="handler">The state handler.</param>
/// <exception cref="System.ArgumentNullException">Thrown when the <paramref name="handler"/> is null.</exception>
protected void Register<TEvent>(Action<TEvent> handler)
{
if (handler == null) throw new ArgumentNullException("handler");
_router.ConfigureRoute(handler);
}
/// <summary>
/// Routes the specified <paramref name="event"/> to a configured state handler, if any.
/// </summary>
/// <param name="event">The event to route.</param>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="event"/> is null.</exception>
public void Route(object @event)
{
if (@event == null) throw new ArgumentNullException("event");
_router.Route(@event);
}
/// <summary>
/// Applies the specified event to this instance and invokes the associated state handler.
/// </summary>
/// <param name="event">The event to apply.</param>
protected void Apply(object @event)
{
if (@event == null) throw new ArgumentNullException("event");
_applier(@event);
}
}
The routing of events during behavior execution follows the contours of the life cycle of the entity: creation, modification, and deletion. During creation, the Apply method creates a new entity (remember this is the only place we're allowed to change state) and assigns it to a field, adds it to a list, dictionary or custom collection. Modification in the Apply method usually involves lookup of the affected entity or entities, and either routing the event to the entity or having dedicated internal methods on the entity that apply the change using data from the event. When deleting, the Apply method does a lookup and removes the affected entity or entities. Notice how these Apply methods blend with the rehydration phase to get to the same state.
Now, it's important to understand that there might be other behaviors that affect entities but do not "belong" to any particular entity (there's no one to one mapping so to speak). It's just stuff that happens and has a side effect on one or more entities. It's these kind of things that make you appreciate entity design flexibility.