modular-monolith-with-ddd
modular-monolith-with-ddd copied to clipboard
Alternative way of initializing aggregate roots in a repository?
Hi Kamil, thank you for this awesome resource. I'm using it as a reference for a project not developed in C# and I'm struggling with the initialization of already persisted entities. The way I'm understanding your codebase is that the EF Framework initializes the entities by using the default private constructor and then sets the properties through reflection.
Another way of doing this would be obviously for the repository to use all the public methods to initialize the entity, but that would require all validations to be run on every database retrieval and also the repositories would become very complex, by having to call the correct entity methods in the correct order, depending on the persisted object state. But on the other side if your persisted data changes for some reason, you could end up with an entity in an invalid business state, couldn't you? One could add a method to the entity e.g. InitializeWithoutValidation(fullData) but that's also not nice. Are you aware of another agreed way of doing this which would not depend on language features?
-- Also unrelated question: I think your IntegrationEventGenericHandler.cs should not use a simple INSERT INTO since InboxMessages.Id is a UNIQUEIDENTIFIER which would mean you would get an exception the second time the same event is sent through the event bus (e.g. when ProcessOutboxCommand fails to update the event state as processed) which would then again prevent outboxCommand from marking the event as sent therefore resulting in a communication deadlock?
The way I'm understanding your codebase is that the EF Framework initializes the entities by using the default private constructor and then sets the properties through reflection.
Yes, your understanding is correct. This how it works.
Another way of doing this would be obvious for the repository to use all the public methods to initialize the entity, but that would require all validations to be run on every database retrieval and also the repositories would become very complex, by having to call the correct entity methods in the correct order, depending on the persisted object state. But on the other side if your persisted data changes for some reason, you could end up with an entity in an invalid business state, couldn't you?
This is not a good idea. Recreating the state of Aggregate from the database and creating Aggregate in code are different things. This is why I use Factory Methods. "A law does not apply retroactively" - it means if I persisted Aggregate I don't want to revalidate it during loading.
One could add a method to the entity e.g. InitializeWithoutValidation(fullData) but that's also not nice.
What you described is called "Memento pattern" and I think that's not a bad solution. This enables full persistence ignorance of your Domain Model, but of course with trade-offs. Google this pattern and check - maybe it will fit your requirements.
Are you aware of another agreed way of doing this which would not depend on language features?
- Serialize and keep whole data as JSON in the database.
- Event sourcing
Also unrelated question: I think your IntegrationEventGenericHandler.cs should not use a simple INSERT INTO since InboxMessages.Id is a UNIQUEIDENTIFIER which would mean you would get an exception the second time the same event is sent through the event bus (e.g. when ProcessOutboxCommand fails to update the event state as processed) which would then again prevent outboxCommand from marking the event as sent therefore resulting in a communication deadlock?
You mean there is no exception handling for this scenario? You are right, it could be handled better like catching the error and log something.
Nevertheless, this is one of the solutions of messages deduplication: insert it to store and use its ID as a unique key.
Thank you for pointing out the memento pattern and the part about the law not applying retroactively, very useful.
As for the other question I was not talking about error handling, I just saw the insert into with the unique constraint and was confused since it is not clear where the exception would be handled, so I assumed it would propagate back to the ProcessOutboxCommandHandler causing the update as processed query not to be executed again. Since if it's catched somewhere in-between then how would you know that the event was saved to the inbox on the other side and it is safe to mark the outbox one as processed? But I did not play with the code so I'm most likely wrong. However it would be nicer if the IntegrationEventGenericHandler made it more explicit that duplicates are to be expected since it is currently rather hidden behind the sql command and only mentioned in the docs. Feel free to close this issue.