Skip to content

[#4362] Allow for commands without an Entity to succeed#4388

Open
smcvb wants to merge 4 commits intoaxon-5.1.xfrom
bug/4362
Open

[#4362] Allow for commands without an Entity to succeed#4388
smcvb wants to merge 4 commits intoaxon-5.1.xfrom
bug/4362

Conversation

@smcvb
Copy link
Copy Markdown
Contributor

@smcvb smcvb commented Apr 3, 2026

This pull request aims to resolve #4388, which is done in two steps.

Step 1 - Ensure the EntityCommandHandlingComponent deals with EntityIdResolutionException

The original issue stems from the EntityIdResolutionException thrown by the automatically set EntityIdResolver that looks for the @TargetEntityId annoted field/value.
However, commands creating an entity are not inclined to hold this identifier at all. An entity could very well invoke a service to create the identifier for the entity in question.
Furthermore, the predicament triggered for a static command handler INSIDE an entity. When the static command handler is placed in a regular component, the issue did not trigger (because the EntityIdResolver wasn't invoked.

The combat this scenario, the EntityCommandHandlingComponent now has a try-catch around the EntityIdResolver invocation.
When an EntityIdResolutionException is thrown, the EntityCommandHandlingComponent will "guess" that the command in question is a creational command handler.
As such, it invokes the EntityMetamodel#handleCreate right away.

When the EntityMetamodel invocation fails with a NoHandlerForCommandException, we can be certain we were not dealing with a creational command handler.
Hence, we fallback to throw the original EntityIdResolutionException from the EntityCommandHandlingComponent.
However, when no NoHandlerForCommandException, the guess was correct.

Step 2 - Make an AppendCondition when none is present on EventStoreTransaction#appendEvent

Providing the fix in step 1 clarified another predicament.
There currently is no means to consciously set the AppendCondition when appending events.
Axon Framework resolves this by constructing an AppendCondition based on an EventStoreTransaction#source invocation.

This works fine, if we have something to source.
In step 1, we effectively remove any sourcing invocation by forcefully taking the EntityMetamodel#handleCreate route.
Hence, we need to have a fallback for this scenario.

Luckily, the DefaultEventStoreTransaction invokes the event-tagger lambda to construct TaggedEventMessage instances out of the given EventMessage to EventStoreTransaction#appendEvent.
If the TaggedEventMessage contains Tags, we can thus assume the user intends to append events that belong to a certain consistency boundary.

A clear example are creational events that have been published as result of creational commands that do not have a @TargetEntityId (or equivalent) annotated field/value.

To comply to that scenario, the DefaultEventStoreTransaction checks if the current ProcessingContext contains an AppendCondition.
If so, it leaves the AppendCondition as is, as it's been set as part of sourcing.
When it is null and we have Tags, we can construct a origin-based AppendCondition with the given tags.
We cannot assume any other consistency marker than origin, as the only means to get a concrete consistency marker is by sourcing.

Round-up

To test the above behavior, I have adjusted one of our integration tests by changing the creation commands to no longer hold an @TargetEntityId.
Although this means we do not test a create-or-update scenario with this command, I wagered the consequence to be neglibible. Especially given the other tests we have for create-if-missing.
Furthermore, I added some indenting and annotation changes to the touched files while working on this.


By doing the above, this PR resolves #4362

smcvb added 4 commits April 3, 2026 18:04
…utionException

If the EntityCommandHandlingComponent is unable to find an entity-id when handling a command, we may be dealing with a creational command handler. Hence, we should invoke EntityMetamodel#handleCreate. If that's successful, our guess was correct. If this call returns a NoHandlerForCommandException, we were not dealing with a creational command handler at all. In that case, we should return the original EntityIdResolutionException

#4362
When the EventStoreTransaction#appendEvent is invoked and we have tags without the existence of an AppendCondition, that signals a scenario where a creational command handler is invoked to append an event without any preceding source invocation. Thus, we should be able to assume we are dealing with the first event in this case. Making an ORIGIN marker with the found tags is restrictive, but should suffice in this case.

#4362
Adjust tests to not have a resolvable identifier for creation. Although this means a shift, there's arguably more value in testing the suggested approach (to not have identifiers to load an entity when the command handler is static) then the other way around. This shift has shown the test cases to inconsistently set creational command handlers. Furthermore, the custom EntityIdResolver does not align. Lastly, the validation changes to an AppendEventsTransactionRejectedException for duplication, as the appendCondition is violated in these cases

#4362
- Resolve nullability of getting a resource
- Clarify updating a resource may mean you need to deal with null
- Fix ugly indentation as a result of non-null/nullability settings
- Clarify the ManagedEntity may not find an entity to load

#4362
@smcvb smcvb added this to the Release 5.1.0 milestone Apr 3, 2026
@smcvb smcvb self-assigned this Apr 3, 2026
@smcvb smcvb requested a review from a team as a code owner April 3, 2026 16:28
@smcvb smcvb added the Type: Bug Use to signal issues that describe a bug within the system. label Apr 3, 2026
@smcvb smcvb requested review from corradom and hatzlj and removed request for a team April 3, 2026 16:28
@smcvb smcvb added the Priority 1: Must Highest priority. A release cannot be made if this issue isn’t resolved. label Apr 3, 2026
@smcvb smcvb requested a review from MateuszNaKodach April 3, 2026 16:28
Copy link
Copy Markdown
Contributor

@hatzlj hatzlj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good to me, no blockers

Comment on lines -41 to -43
if (identifier != null) {
throw new IllegalStateException("Employee is an existing entity");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having switched to org.axonframework.modelling.entity.EntityMetamodelBuilder.creationalCommandHandler we don't need to check this anymore because the Metamodel makes sure a null entity is returned from the repository, right?

if (!tags.isEmpty()) {
// No AppendCondition is present, but the event contains tags.
// Tags make no sense without an AppendCondition, so let's create an ORIGIN call
processingContext.updateResource(appendConditionKey, current -> current == null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not #computeResourceIfAbsent

@smcvb smcvb added the Status: Under Discussion Use to signal that the issue in question is being discussed. label Apr 7, 2026
@smcvb
Copy link
Copy Markdown
Contributor Author

smcvb commented Apr 7, 2026

The failing test tests something important: publishing two events during up front. Granted, it does this in our Test Fixtures. Never the less, I think we should have a test validating that a command handler that does not source an entity can also append several tagged events without issue, as this will be a common scenario.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority 1: Must Highest priority. A release cannot be made if this issue isn’t resolved. Status: Under Discussion Use to signal that the issue in question is being discussed. Type: Bug Use to signal issues that describe a bug within the system.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants