Managing Child Collections¶
It is quite common that our Aggregate Roots have a list of child entities. For example, a User Group has a list of Users, a Spare Request has a list of lines, etc.
As we covered in Idempotence, it is important that messages themselves are idempotent so that a consumer can be idempotent.
In order to make our messages idempotent when dealing with collections, we will receive the final list of child entities that the Aggregate Root should have. That is the easiest way to make our consumers idempotent, because if we were to consume a message twice, the Aggregate Root would end up with the same list of child entities.
Customizing the domain¶
In our domain, the Equipment
is the Aggregate root. We will add a relation
with a MaintenanceTask
entity which is just a name for now.
Now that we have our MaintenanceTask
Entity, we can modify our Equipments
entity to have a list of MaintenanceTask
s.
C# | |
---|---|
Tip
Always use ICollection<T>
when you need an abstraction over a list that
has add/remove methods.
Use IEnumerable<T>
when you just need to loop through the results.
Customizing Persistence¶
In the Domain, a MaintenanceTask
cannot exist by its own without it being
related to an Equipment
. We can say that the Equipment
OwnsMany
MaintenanceTask
s.
EFCore supports this with Owned Entities
In our Equipment
class mapping, we need to:
We also need to create a new EntityTypeConfiguration
for our MaintenanceTask
that does:
Customizing the Application¶
Let's add support for relating Equipments to MaintenanceTask
s. Like we
mentioned previously, we wanna apply what the incoming message says to our
Aggregate Root. The state of the message is always the final state of the
entity.
Our message could look like this:
C# | |
---|---|
Now in our CreateOrUpdateEquipmentConsumer
we need to apply the changes from
the message to our entity. Usually, that would be as simple as doing something
like:
C# | |
---|---|
However, if you have worked with an ORM like EntityFramework or NHibernate you would know that this will most certainly end up in deleting all rows and inserting them when changes are applied to the database.
In order to avoid that, we can't just Clear
and recreate the collection. Nor
we can assign a new collection instance. We need to:
- Create new
MaintenanceTask
s - Update existing
MaintenanceTask
s with the newJob
- Delete
MaintenanceTask
s
In order to simplify this process, the Suite Framework includes a
CollectionAdapter<TEntity, TInput>
that we can use.
The adapter works by receiving a reference to the collection that we want to
update and an implementation for CreateEntity
, UpdateEntity
and Compare
.
The adapter's job is to compare the incoming inputs against the Aggregate Root's
collection. It does so by executing Compare
against all elements.
It will then call CreateEntity
when there are entities in the input not
present in the Aggregate Root. It will call UpdateEntity
for entities already
present in the Aggregate Root.
It will also Remove
entities from the Aggregate Root that are no longer
present in the input collection. This can be customized by overriding
DeleteEntity
, however a default implementation is provided.
The implementation of the collection adapter should be placed in the Application layer.
The adapter can be unit tested, however we recommend just testing the consumer. We don't care how the consumer does the job, so long as it does so.
Now, in our consumer we need to instantiate the adapter and ApplyChanges
using
the input.
Support for relations between Aggregate Roots¶
In the previous example, Equipments
ownsMaintenanceTask
s. The tasks
themselves cannot exist without them being related to an Equipment
.
Equipment
is the Aggregate Root and the MaintenanceTask
is a child entity.
In many cases we do need to have an Aggregate Root related to another Aggregate
Root, in which case the first Aggregate Root has a reference to another
Aggregate Root, without owning it. For example, we could have an
EquipmentGroup
which has a list of Equipment
s. Let's quickly model that:
Our Equipment
entity, needs to have a reference to its EquipmentGroups
it
belongs to. Let's model it as a many-to-many:
C# | |
---|---|
In the EntityTypeConfiguration for Equipment
, we need to:
In our application layer, we would receive an input message like this:
C# | |
---|---|
In the consumer, we can use the CorrelatedEntityRelationalCollectionAdapter
,
as long as our child entity implements ICorrelatedEntity
.
The consumer only needs to ApplyChanges
to the adapter, which we instantiate
giving it a reference to the collection and the Equipment
repository.
The adapter will do a single DB Query to fetch all Equipments
and then it will
add them to the collection, when they are not present. It will remove them from
the collection when they are no longer present in the input, and will do nothing
in updates.