javaspringspring-bootaggregateaxon

Axon CommandHandlers in AggregateMember Not found


I have an aggregate with multiple AggregateMembers. No is the problem that the for 1 aggregateMember (Referentschap) the CommandHandlers in this aggregate member are not found. This problem also occurs with junit testing using the AggregateTestFixture<>(Onderzoek.class).

In one AggregateMember Waarschuwing the CommandHandlers are reached as it should, but in the other aggregateMember Referentschap the Commandhandlers are not found. My question is how can i work around this problem? Is there a way that i can make a configuration to fix this?

Curious issue is that in a copy of the project for some debugging, the same issue arises but then the problem is swapped to Waarschuwing and is Referentschap ok. This curious swap of problems gives me the impression that there is an issue in how the CommandHandlers are collected.

update: On a hint of Axoniq is placed only the @Aggregae annotation on the root aggregate. Cleared chaces and all from my Intellij IDE and rerun the junit test. Now the not found ComandHandler issue rises on both the AggregateMembers Referentschap and Waarschuwing.

I have made some integration test with an Axon testcontainer and they al succeed. As it seems is it a JUnit test problem, where the order of extending of aggregate classes is relevant for which CommandHandler is found.

Axon 4.5.17
Spring boot 2.7.12
Java temurin 17.0.6

@NoArgsConstructor
public class Onderzoek extends OnderzoekReferentschap {

    @CommandHandler
    public Onderzoek(InitieerOnderzoek command) {
        apply(OnderzoekGeinitieerd.builder().build());
    }

    @EventSourcingHandler
    public void onderzoekGeinitieerd(OnderzoekGeinitieerd event) {
        this.onderzoekId = event.getOnderzoekId();
        this.onderzoekStatus = INITIEEL;
    }
@NoArgsConstructor
public abstract class OnderzoekReferentschap extends OnderzoekMaatregel {

    @AggregateMember
    private final Map<UUID, Referentschap> referentschappen = new HashMap<>();

    @CommandHandler
    public void verwerkReferentschapOntstaan(VerwerkReferentschapOntstaan command) {
            apply(ReferentschapOntstaanVerwerkt.builder()
                    .onderzoekId(command.getOnderzoekId())
                    .referentschapId(command.getReferentschapId())
                    .build());
        }
    }

    @EventSourcingHandler
    public void referentschapOntstaanVerwerkt(ReferentschapOntstaanVerwerkt event) {
        UUID referentschapId = event.getReferentschapId();
        Referentschap referentschap = Referentschap.builder()
                .referentschapId(referentschapId)
                .persoonId(event.getPersoonId())
                .build();

        referentschappen.put(referentschapId, referentschap);
    }
}
@NoArgsConstructor
abstract class OnderzoekMaatregel extends OnderzoekBasis {
    @AggregateMember
    final Map<UUID, Waarschuwing> waarschuwingen = new HashMap<>();

    @CommandHandler
    public void verwerkWaarschuwingOpgelegd(VerwerkWaarschuwingOpgelegd command) {
            apply(WaarschuwingOpgelegdVerwerkt.builder()
                    .onderzoekId(command.getOnderzoekId())
                    .referentschapId(command.getWaarschuwingId())
                    .build());
    }

    @EventSourcingHandler
    public void waarschuwingOpgelegdVerwerkt(WaarschuwingOpgelegdVerwerkt event) {
        UUID waarschuwingId = event.getWaarschuwingId();
        waarschuwingen.put(waarschuwingId, new Waarschuwing(waarschuwingId));
    }
}
@NoArgsConstructor
abstract class OnderzoekBasis {

    @AggregateIdentifier
    UUID onderzoekId;
    OnderzoekStatus onderzoekStatus;
}

The following class are the AggregateMembers with the troublesome Not Found CommandHandlers.

@Aggregate
@EqualsAndHashCode
@NoArgsConstructor
public class Referentschap {

    @EntityId
    private UUID referentschapId;

// This command handler is never reached.
    @CommandHandler
    public void verwerkReferentschapBijgewerkt(VerwerkReferentschapBijgewerkt command) {
        apply(ReferentschapBijgewerktVerwerkt.builder()
                .onderzoekId(command.getOnderzoekId())
                .referentschapId(command.getReferentschapId())
                .einddatum(command.getEinddatum())
                .build());
    }

@Aggregate
@EqualsAndHashCode
@NoArgsConstructor
public class Waarschuwing {

    @EntityId
    private UUID waarschuwingId;

// this command handler is never reached.
    @CommandHandler
    public void verwerkWaarschuwingVerlopen(VerwerkWaarschuwingVerlopen command) {
        apply(WaarschuwingVerlopenVerwerkt.builder()
                .onderzoekId(command.getOnderzoekId())
                .waarschuwingId(command.getWaarschuwingId())
                .verloopdatum(command.getVerloopdatum())
                .build());
    }
}

Solution

  • It's a hunch, but I guess Axon Framework's AggregateModel isn't correctly constructed due to the @Aggregate annotation on your Aggregate Members too.

    Aggregate Members should only be referenced within the aggregate through the @AggregateMember annotation. The @Aggregate annotation is there to:

    1. Mark a class as an Aggregate, and
    2. Let it be auto-wired through Axon's Spring auto-configuration.

    If you have a "simple" aggregate, you only need to use the @Aggregate annotation once, on the so-called Aggregate Root. If you have a polymorphic aggregate, you only have to use the @Aggregate annotation on the root class and all concrete implementations (essentially the children of the graph).

    Any classes that are not a root aggregate or polymorphic aggregate do not need the @Aggregate annotation. My hunch that this is the culprit stems from the fact the @Aggregate annotation performs the auto-wiring and is used for polymorphic aggregate logic, by the way.

    So, please remove the annotation from your members and tell us whether that solves the problem!

    Update

    After receiving the sample project through a different channel, I was able to debug the scenario @Jan was facing. The Aggregate structure turned out to contain two inconsistencies we had to deal with.

    One - Polymorphic Aggregate Members

    First and foremost, the sample project contains an aggregate with polymorphic entities (read: @AggregateMember annotated fields inside the aggregate). Currently, Axon Framework does not investigate the entire hierarchy of the Type of the field that's annotated to be an entity.

    Due to this, Axon Framework will only discover message-handling methods on the actual annotated field, not on any other layer in the hierarchy.

    To clarify, regard the following sample:

    public class AggregateRoot {
        @AggregateMember
        private RootEntity entity
        // omitting message handlers for simplicity
    
    }
    
    public RootEntity {
        // omitting message handlers for simplicity
    }
    
    public ChildEntity {
        // Message handlers here are not found!
    }
    

    The AggregateRoot aggregate class has an entity field of type RootEntity. By annotating it with @AggregateMember, you tell Axon Framework, "this class has message handlers you need to register with the message buses." However, this support does not extend to the implementation of the RootEntity.

    Two - Aggregate Members on each level in the Aggregate Hierarchy

    The second predicament had to do with the fact Entities/Aggregate Members were present on several layers in the Aggregate hierarchy. Axon Framework would only move one layer down the (so-called) "child entity set" to uncover command handlers to register with the internal Aggregate Model.

    However, it is perfectly valid to have Aggregate Members on several layers of the Aggregate hierarchy. Due to this bug, the Framework would thus miss aggregate members during the construction of the internal model.

    As this was a bug, a pull request was provided to fix this, which you can find here. This fix is part of Axon Framework release 4.8.1, by the way.