I am looking to see what approaches people might have taken to detect changes in entities that are a part of their aggregates. I have something that works, but I am not crazy about it. Basically, my repository is responsible for determining if the state of an aggregate root has changed. Let's assume that I have an aggregate root called Book
and an entity called Page
within the aggregate. A Book
contains one or more Page
entities, stored in a Pages
collection.
Primarily, insert vs. update scenarios are done by inspecting the aggregate root and its entities to determine the presence of a key. If the key is present, it is presumed that the object has been, at one time, saved to the underlying data source. This makes it a candidate for an update; but it is not definitive based upon that alone for the entities. With the aggregate root the answer is obvious, since there is only one and it is the singular point of entry, it can be assumed that key presence will dictate the operation. It is an acceptable scenario, in my case, to save the aggregate root itself back again so that I can capture a modification date.
To help facilitate this behavior for the entities themselves, my EntityBase
class contains two simple properties: IsUpdated()
, IsDeleted()
. Both of these default to false. I don't need to know if it is new or not, because I can make that determination based upon the presence of the key, as mentioned previously. The methods on the implementation, in this case the Page, would have each method that changes the backing data set IsUpdated()
to true.
So, for example, Page has a method called UpdateSectionName()
which changes the backing value of the SectionName
property, which is read-only. This approach is used consistently, as it allows for a logical attachment point of validators in the method (preventing the entity from entering an invalid state) that performs that data setting. The end result is that I have to put a this.IsUpdated() = true;
at the end of the method.
When the aggregate root is sent into the repository for the Save()
(a logic switch to either an Insert()
or Update()
operation), it can then iterate over the Pages
collection in the Book
, looking for any pages that have one of three scenarios:
Page
with no key will be inserted.IsDeleted = true;
A delete trumps an update, and the deletion will be committed - ignoring any update for the Page
.IsUpdated = true;
An update will be committed for the Page.Doing it this way prevents me from just blindly updating everything that is in the Pages collection, which could be daunting if there were several hundred Page entities in the Book, for example. I had been considering retrieving a copy of the Book, and doing a comparison and only committing changes detected, (inserts, updates, and deletes based upon presence and/or comparison), but it seemed to be an awfully chatty way to go about it.
The main drawback is that the developer has to remember to set IsUpdated in each method in the entity. Forget one, and it will not be able to detect changes for that value. I have toyed with the idea of some sort of a custom backing store that could transparently timestamp changes, which could in turn make IsUpdated
a read-only property that the repository could use to aggregate updates.
The repository is using a unit of work pattern implementation that is basing its actions on the timestamp generated when the aggregate root was added to it. Since there might be multiple entities queued for operations, entity operations are rolled up and executed immediately after the aggregate root operation(s) are executed that the entities belong to. I could see taking it a step further and creating another unit of work to just handle the entity operations and base them off some sort of event tracking used in the entity (which is how I am assuming that some of the ORM products on the market accomplish a similar level of functionality).
Before I keep on moving in this direction, though, I would love to hear ideas/recommendations/experiences regarding this.
Edit: A few additional pieces of information that might be helpful to know:
In short, my answer is that I went with what I proposed. It is working, although I am sure that there is room for improvement. The changes actually took very little time, so I feel I didn't navigate too far from the KISS or YAGNI principals in this case. :-)
I still feel that there is room for timing related issues on operations, but I should be able to work around them in the repository implementations. Not the ideal solution, but I am not sure that it is worth reinventing the wheel to correct a problem that can be avoided in less time than it takes to fix.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With