I’m migrating a Spring Boot app written with Kotlin from AF 4.13 to AF 5.3.1. I’m trying to recreate the behaviour of throwing an exception when the event-sourced entity (formerly aggregate) is marked for deletion. For that, I have added a CommandHandlerInterceptor on the entity like this:
@CommandHandlerInterceptor
fun denyCommandsOnRedeemedGiftCard(commandMessage: CommandMessage) {
if (isRedeemed) {
throw IllegalStateException("Gift card already redeemed")
}
}
When testing this with a unit test, I never see the interceptor being called for any command. What could be going wrong here?
The test looks like this:
class GiftCardTest {
private lateinit var fixture: AxonTestFixture
@BeforeEach
fun setUp() {
fixture = AxonTestFixture.with(
EventSourcingConfigurer
.create()
.registerEntity(
EventSourcedEntityModule.autodetected(String::class.java, GiftCard::class.java),
)
)
}
@AfterEach
fun tearDown() {
fixture.stop()
}
@Test
fun denyCommandsOnRedeemedGiftCard() {
val giftCardCode = UUID.randomUUID().toString()
fixture
.given()
.command(IssueGiftCard(giftCardCode, 50))
.events(GiftCardRedeemedEvent(giftCardCode))
.`when`()
.command(RedeemGiftCard(giftCardCode))
.then()
.exception(IllegalStateException::class.java)
}
}
Thanks for your message. Looks like you have found a gap between AF4 and AF5: a @CommandHandlerInterceptor declared inside the entity (the AF4 aggregate style) is currently not invoked on the command path.
To close this gap will take a little time and may land in a future release. We need to discuss that internally. Let me get back to you on Monday.
Do you have a lot of places like that or is it for a single aggregate?
Because the simplest workaround I see right now, if you need to access entity state (if not interceptors which are out of the entity should be invoked as expected) to make the decision is to put it in each @CommandHandler.
Another possibility is that you can move your CommandHandlers out of the entity class using Stateful Command Handlers
In a long term is the approach I recommend, because it allows using the DCB to have commands that form it’s own consistency boundary or modify multiple entities transactionally. Then you should be able to use @CommandHandlerInterceptor along with @InjectEntity.
class GiftCardCommandHandler {
@CommandHandlerInterceptor
fun check(
command: CommandMessage,
@Nullable @InjectEntity state: GiftCard
) {
// Runs for every command handled by this component.
}
@CommandHandler
fun handle(
command: RedeemCard,
@Nullable @InjectEntity state: GiftCard,
appender: EventAppender
) {
appender.append(CardRedeemed(command.id))
}
}
Slight one-up here from what @MateuszNowak shared with you, @pstek.
This is a gap we, sadly, did not spot ourselves when reintegrating interceptor support into Axon Framework 5.
As such, I’ve put some effort into fixing this misser ASAP.
The PR for that is out just now and can be found here.
The referred to PR is for 5.4.0 (to be released end of September / start of October), but will receive a back-port to 5.3.2 once reviewed, approved, and merged.
I’d expect 5.3.2 to be released somewhere this week.