Skip to content

AF5 model inspection - expose AF5 entity inspection over RSocket#146

Open
stefanmirkovic wants to merge 10 commits into
mainfrom
feature/af5-model-inspection
Open

AF5 model inspection - expose AF5 entity inspection over RSocket#146
stefanmirkovic wants to merge 10 commits into
mainfrom
feature/af5-model-inspection

Conversation

@stefanmirkovic
Copy link
Copy Markdown
Contributor

@stefanmirkovic stefanmirkovic commented Apr 20, 2026

Adds RSocketModelInspectionResponder with four endpoints backed by AF5's StateManager and EventStorageEngine:

  • MODEL_REGISTERED_ENTITIES: entity types + ID types + structural id-field descriptors from StateManager (drives the dynamic Entity ID form on the frontend)
  • MODEL_DOMAIN_EVENTS: paginated events for a given entity
  • MODEL_ENTITY_STATE_AT_SEQUENCE: state reconstruction at any sequence
  • MODEL_REPLAY_TIMELINE: chunked timeline (offset + limit) with stateBefore / event / stateAfter per entry

Tag keys are resolved from @eventsourced / @EventSourcedEntity annotations (including meta-annotations), falling back to the class's simple name. State reconstruction walks the full event stream to keep stateBefore accurate for the first event in the requested window, but only serializes snapshots for entries in [offset, offset + limit), bounding payload size regardless of stream length.

AxoniqPlatformModelInspectionEnhancer registers the responder via the ConfigurationEnhancer SPI. SetupPayloadCreator now reports hasStateManager so the platform can detect model inspection support.

Compound id + multi-tag support

Replaces the hand-crafted EventCriteria.havingTags(Tag.of(tagKey, entityId))
(single-tag only, breaks for multi-tag entities) with a delegation to the
framework's own CriteriaResolver:

  • resolveCriteria(entityType, entityId) extracts the live resolver from the
    registered EventSourcingRepository via reflection, honoring any custom
    resolver the application has configured. Falls back to constructing a fresh
    AnnotationBasedEventCriteriaResolver(entityClass, idClass, configuration)
    • the AF5 default path - and only as a last resort to the legacy single-tag
      lookup
  • Resolvers are cached per (entityClass, idClass) pair
  • describeIdFields(idClass) introspects records, Kotlin data classes / POJOs,
    and @JvmInline value class wrappers. Simple types (String/primitives/UUID/
    enums/BigInteger/BigDecimal) return an empty descriptor list so the frontend
    keeps a single input
  • deserializeEntityId handles primitives directly, unwraps Kotlin value
    classes through their public constructor, and lets Jackson deserialize
    JSON-encoded compound ids into the real typed id before invocation
  • Responder now also takes a Configuration parameter (wired through the
    enhancer) so it can build resolvers on demand
  • ProcessingContext is passed as null - verified safe for the default
    annotation-based resolver which doesn't read it; custom resolvers that do
    throw NPE, which is caught and falls back to the legacy path (no regression)

Relates: https://github.com/AxonIQ/axoniq-platform/pull/538

@stefanmirkovic stefanmirkovic self-assigned this Apr 20, 2026
@stefanmirkovic stefanmirkovic added the Backend This issue contains backend changes label Apr 20, 2026
…pection

# Conflicts:
#	framework-client/src/main/java/io/axoniq/platform/framework/eventsourcing/AxoniqPlatformModelInspectionEnhancer.java
#	framework-client/src/main/java/io/axoniq/platform/framework/eventsourcing/RSocketModelInspectionResponder.kt
#	framework-client/src/main/resources/META-INF/services/org.axonframework.messaging.core.annotation.HandlerEnhancerDefinition
@CodeDrivenMitch CodeDrivenMitch changed the base branch from release/2.0.5-SNAPSHOT to main April 24, 2026 09:03
Copy link
Copy Markdown
Collaborator

@CodeDrivenMitch CodeDrivenMitch left a comment

Choose a reason for hiding this comment

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

It's heading in the right direction, but I still see some problem with how the id types would be used in the frontend. There's a big assumption that there will only be one id, which is not the case.

This is a proposal PR to the original PR posed by Mirko, #146. It takes a different approach to the same problem, and functionally nothing has changed

## Why the proposal

The original approach was very reflection-heavy, which is partly to be expected, but can also be improved upon. The more inner reflection is present, the more likely it's to break.

In addition, the approach didn't account for entites in submodules for hierarchic contexts. As such, only top-level entities would be discovered.

As this all requires very deep knowledge about how the Configuration of Axon Framework works, I decided to make this proposal PR to guide by example, instead of separate comments.

## Basics of the rewrite

1. The `UnitOfWorkFeactory.create()` is used during all sourcing and read operations. This way all parameter resolvers will work correctly. This takes away the need of manually invoking methods.
2. Repository instances are discovered at boot time. If it's an EventSourcingRepository, it's registeredd to the `RSocketModelInspectionResponder`
3. The `EventSourcingRepository` is rebuilt using reflection to be able to decorate the `EntityEvolver` with the `AxoniqPlatformEntityEvolver`.
4. Reading the event stream and entity state is now a `Repository.load(...)` operation. Several resources are put in the `ProcessingContext`, which the decorators respond to.
  a. `AxoniqPlatformEntityEvolver.BEFORE_CONSUMER`: If present, calls with the entity state before any evolve
  b. `AxoniqPlatformEntityEvolver.AFTER_CONSUMER`: If present, calls with the entity state after any evolve
  c. AxoniqPlatformEntityEvolver.MAX_INDEX: Stops evolving the entity after a certain index.

## Result

With the new code a lot of the reflection-based code could be deleted. The RSocketModelInspectionResponder.kt shrunk by half its size. We have programmed against the interfaces of the framework, which are less likely to change.

In addition I added tests that confirm it works.
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 8, 2026

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

Labels

Backend This issue contains backend changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants