-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Applying domain events to aggregate roots when deserializing payload of old events break current value object constraint changes #42
Comments
Thoughts @prolic ? @matthiasnoback maybe? This reminds me of your example in chapter "3.4. Using value objects with internal read models" of your new book "Web application architecture": You suggest to use read model for a book price: final class Price
{
private int $priceInCents;
private function __construct(int $priceInCents)
{
$this->priceInCents = $priceInCents;
}
public static function fromInt(int $priceInCents): self
{
return new self($priceInCents);
}
public function asInt(): int
{
return $this->priceInCents;
}
} Here we have multiple named constructors. And similar to my example it would get complex if someone suddenly added a "must be greater then zero" constraint to the constructor but for some reason negative numbers were valid in the past and these values still exist in the event stream for this read model. |
Interesting read by @heynickc :
|
Would love to hear how the @AxonFramework deals with these kind of "value object from deserialized event payload without revalidating" issue. @abuijze Could you give us some feedback on this? Thanks in advance. |
Related to an older issue: Thanks to @Ocramius for his feedback:
|
My current suggestions would be: private function __construct()
{}
public static function fromString(string $name): FirstName
{
$name = NameNormalizer::withString($aName);
$self = new self();
$self->validate();
$self->name = $name;
return $self;
}
public static function fromWhateverWithValidationRequired(string $name): FirstName
{
$name = NameNormalizer::withString($aName);
$self = new self();
$self->validate($name);
$self->name = $name;
return $self;
}
private function validate(string $name): void
{
if (!NamePolicy::isSatisfiedBy($name)) {
throw new NameContainsIllegalCharacters();
}
}
public function fromPayload(string $payload): FirstName // or `fromEventPayload` or `deserialize`?!
{
$self = new self();
$self->name = $name;
return $self;
} or private function __construct(string $aName)
{
$name = NameNormalizer::withString($aName);
if (!NamePolicy::isSatisfiedBy($name)) {
throw new NameContainsIllegalCharacters();
}
$this->name = $name;
}
public static function fromString(string $name): FirstName
{
return new self($name);
}
public static function fromPayload(string $name): FirstName // or `fromEventPayload` or `deserialize`?!
{
$firstNameRef = new \ReflectionClass(\get_called_class());
/** @var FirstName $firstName */
$firstName = $firstNameRef->newInstanceWithoutConstructor();
$firstName->name = $name;
return $firstName;
} Thoughts? |
You can use a special constructor for this that is marked |
Other options related to the library or framework you use could be versioning of events as suggested by @Mattin. Options would be:
Regarding versioning of events in @prooph you could simply add your version inside your events and then store it inside the metadata just like the "aggregate_version". Related: |
At the bottom line a totally I agree with @AntonStoeckl here:
This example is not critical and the old data does not have to "fixed". Otherwise e.g. with GDPR a change of the event stream may be neccessary. As suggested by @bwaidelich. Here applying old events to the aggregate root should not break anything. The EVENT itself has not changed - that why Event Upasting is not really the best approach. The DOMAIN changed. As explained by @AntonStoeckl here:
|
I think this question can only be answered if you know the intentions of the change: a) It's a critical new invariant that has to be enforced by all means b) It's a new rule that should be enforced during the creation of new events but can be ignored when consuming them |
But why create a new value object? The constraints can simply be added to the existing ones. The are added NOW. If the event stream is applied to the aggregate root OLD and NEW data use a named constructor e.g. "fromPayload" that DOES NOT RE-VALIDATE. Since it has already been validated the day they were created and WERE VALID at that time. The I like the idea of marking it as |
Because, if you ignore the new constraints you don't gain anything from them. Also it would get really confusing if you later add constraints that should be always enforced.. I'm still not perfectly sure whether I got the reason for adding the new constraints right. But it seems it's all about the write side anyways (prevent users from creating new names with numbers). In the UI: <input type="text" pattern="[a-zA-Z]{2,20}" required /> or in the command (handler). And, btw, this was probably just an example. But Elon Musks daughter wouldn't be allowed to use your service ;) |
That is the point! The domain logic does not have to know it. NEW data will be passed to the VO and validated for sure. protected function apply(AggregateChanged $event): void
{
switch (get_class($event)) {
case NameChanged::class:
/** @var NameChanged $event */
$this->firstName = $event->newFirstName(); These primitives must NOT be re-validated since:
At the bottom line it is just a matter of de-/serialization of primitives in event payload. This can be achieved by the suggestions above. Hope this helps to nail the original issue. :) |
Came from:
Example:
/cc @AntonStoeckl
Our
FirstName
value object has changed over time. It started with no constraints (FirstName_V1). That's whay event payload like the following was possible:Later constraints e.g.
no numbers allowed
were added (FirstName_V2). The names were fixed. Now the event history that has to applied to thePerson
aggregate root includes this:When creating the event and serializing the data this will not cause any problem.
But when applying it...
...it will break and throw the
NameContainsIllegalCharacters
exception.There are two solutions I can imagine so far:
(1) Somehow catch the date of the BC for the payload and convert the old breaking payload to the new constraints. Similar to "event upcasting".
(2) Add a named constructor e.g.
fromPayload
that skips the validation. Similar to value objects based on multiple properties that have anfromArray
andtoArray
e.g.:Source: https://github.com/prooph/common/blob/master/src/Messaging/DomainMessage.php#L49-L65
This example even uses reflection in order to skip the constructor.
Concrete solution:
Disadvantage: the validation is no longer inside the constructor. This is fine as long there is only a single named constructor that requires it. If there are multiple methods I guess you would have to put them into an extra
validate
method.Please note:
Normally a first name is not a critical property that needs to be stored inside an aggregate root since it is not relevant when protecting the invariants when changing state. But let's assume it is in example. That's why I added the old first name to the event.
The text was updated successfully, but these errors were encountered: