Detecting whether an aggregate exists #1804
Replies: 13 comments
-
Hi! Indeed, it looks like boilerplate, but that not quite right. Imagine an aggregate:
We need to define exists automatically under the hood. But here some inconsistencies:
So, my opinion, its more looks like a pattern, or somewhat, but not a boilerplate that can be moved to internals. |
Beta Was this translation helpful? Give feedback.
-
First, I would approach this without changing reSolve - for instance, with some kind of high order function.
If this is something that developers would often forget to do, then we can think of kind of middleware that once registered, will be called before each command handler and projection function Also, I would not enforce such things by default, I can imagine an aggregate that can have no create* command at all - if system doesnt care if it created or just updated, then it can leave without this command. |
Beta Was this translation helpful? Give feedback.
-
I'm not quite sure that my request has been understood correctly. So let me try to clarify two points:
Instead, I want a way to access the information reSolve already holds: is the aggregate passed to the command function a new one (i.e. it has just been created from the initial value and no projections have ever been applied to it) or is it an old one (i.e. has had projections applied)? I believe that reSolve can tell the difference - am I correct? If so, I could imagine a simple flag passed to the function: myCommand: (state, { aggregateId, isNew, payload: ... }) => {
// check isNew to find out whether the aggregate is new
} I'm not sure whether the suggested placement of this flag makes most sense, there might be better ways of passing it. But I hope this serves to clarify what I'm asking. |
Beta Was this translation helpful? Give feedback.
-
Internally aggregate has a version - number of events applied to it before command. |
Beta Was this translation helpful? Give feedback.
-
Yes! That's what I said in my initial post:
I suddenly forgot about it myself, but I believe this detail would be useful - I would leave it up to the developer to do with what they want, and I also agree with you, Roman, that a collection of higher order functions would be the best approach to make "standard" functionality/patterns available over time. |
Beta Was this translation helpful? Give feedback.
-
It seems to be undocumented, but current aggregate version is passing within command context (third argument): doSomething: (state, command, { aggregateVersion }) => {
...
} |
Beta Was this translation helpful? Give feedback.
-
I tested it to be sure and this approach works exactly like it should: {
createThing: (aggregate, { payload: { ... } }, {aggregateVersion}) => {
if (aggregateVersion > 0) throw new Error('Thing exists');
...
},
updateThing: (aggregate, { payload: { ... } }, {aggregateVersion}) => {
if (aggregateVersion === 0) throw new Error('Thing does not exist');
...
},
} Unless this API changes, or there are reasons why it shouldn't be done this way, this solves my problem. I'll close this issue. |
Beta Was this translation helpful? Give feedback.
-
Turns out that a command in aggregate A can be called with an aggregate ID that belongs to an aggregate instance of type B. In such a case, the aggregate version that is passed to the command handler will be that of the B-type instance, but the aggregate instance itself is a new (created from |
Beta Was this translation helpful? Give feedback.
-
To be clear. Let's say I send a command to create an aggregate A:
Now I send a command to create an aggregate B. I use the same ID as before:
I will now find myself in the function In this situation, the |
Beta Was this translation helpful? Give feedback.
-
So... this was converted from an issue a long time ago, but there have never been updates or follow-ups - and the issue was closed a while back. Am I missing something? I have not attempted to follow through all my examples again, but it appears to me that the existing workaround is just as flawed as it was when I opened the issue. (Btw, I have no clue why all the follow-ups above are called "answers" - and the button I'm supposed to click below is titled "Answer" - I guess GitHub discussions don't correctly reflect reality.) |
Beta Was this translation helpful? Give feedback.
-
The information about aggregate type is not stored with the event. Normally, there should not be different aggregates that share the same aggregateId. In this case, aggregateVersion will have the correct value and you can use it as you want. |
Beta Was this translation helpful? Give feedback.
-
Understood. However, I don't entirely agree - to repeat my words from the very beginning of this discussion:
After everything we discussed, I believe this remains true and I stand by it. |
Beta Was this translation helpful? Give feedback.
-
I think it would be useful if there was a built-in mechanism to detect whether an aggregate exists (in a command handler function) - reSolve knows this, since it has just attempted to retrieve the aggregate before the function is called. Unfortunately, the information is not passed on to the command handler and I am left to my own devices to figure it out. Since many aggregates require this logic to avoid duplicates, there is room for improvement here.
Currently, the only "good" option is to handle a special aggregate field which denotes specifically whether the aggregate exists or not. (Note - I'm aware of several other approaches that are worse than this - please let me know in case you need me to elaborate.)
So I'd have this in
thing.commands.js
:And then this in
thing.projection.js
:Obviously this approach works - but I don't think it should be necessary. It's boilerplate, required for almost any aggregate, and all just to detect a piece of information that is already known to reSolve!
I suggest we use one of these two approaches:
Beta Was this translation helpful? Give feedback.
All reactions