diff --git a/acme-data/acme-data-scheduling/src/main/kotlin/com/acme/scheduling/data/JooqAppointmentAggregateRepository.kt b/acme-data/acme-data-scheduling/src/main/kotlin/com/acme/scheduling/data/JooqAppointmentAggregateRepository.kt index 8a2dafd..22a2c0e 100644 --- a/acme-data/acme-data-scheduling/src/main/kotlin/com/acme/scheduling/data/JooqAppointmentAggregateRepository.kt +++ b/acme-data/acme-data-scheduling/src/main/kotlin/com/acme/scheduling/data/JooqAppointmentAggregateRepository.kt @@ -22,25 +22,21 @@ class JooqAppointmentAggregateRepository( private val clock: Clock = Clock.systemUTC() ) : AggregateRepository { - override suspend fun find(id: Appointment.Id): PersistedAggregate? = - dsl.selectFrom(APPOINTMENTS) - .where(APPOINTMENTS.ID.eq(id.value)) - .awaitFirstOrNull()?.let { - PersistedAggregate( - aggregate = Json.decodeFromString(it.aggregate.data()), - metaData = PersistenceMetaData( - createdAt = it.createdAt, - updatedAt = it.updatedAt, - revision = it.revision, + override suspend fun findById(id: Appointment.Id): Result> = + runCatching { + dsl.selectFrom(APPOINTMENTS) + .where(APPOINTMENTS.ID.eq(id.value)) + .awaitFirst().let { + PersistedAggregate( + aggregate = Json.decodeFromString(it.aggregate.data()), + metaData = PersistenceMetaData( + createdAt = it.createdAt, + updatedAt = it.updatedAt, + revision = it.revision, + ) ) - ) - } - - override suspend fun get(id: Appointment.Id): PersistedAggregate = - getOrThrow(id) { NoSuchElementException() } - - override suspend fun getOrThrow(id: Appointment.Id, block: () -> Throwable): PersistedAggregate = - find(id) ?: throw block() + } + } override suspend fun exists(id: Appointment.Id): Boolean = dsl.selectOne().from(APPOINTMENTS).where(APPOINTMENTS.ID.eq(id.value)).awaitFirstOrNull() != null diff --git a/acme-data/acme-data-scheduling/src/main/kotlin/com/acme/scheduling/data/JooqClientAggregateRepository.kt b/acme-data/acme-data-scheduling/src/main/kotlin/com/acme/scheduling/data/JooqClientAggregateRepository.kt index b19338f..daeb741 100644 --- a/acme-data/acme-data-scheduling/src/main/kotlin/com/acme/scheduling/data/JooqClientAggregateRepository.kt +++ b/acme-data/acme-data-scheduling/src/main/kotlin/com/acme/scheduling/data/JooqClientAggregateRepository.kt @@ -20,10 +20,10 @@ class JooqClientAggregateRepository( private val clock: Clock = Clock.systemUTC() ) : AggregateRepository { - override suspend fun find(id: Client.Id): PersistedAggregate? = + override suspend fun findById(id: Client.Id): Result> = runCatching { dsl.selectFrom(CLIENTS) .where(CLIENTS.ID.eq(id.value)) - .awaitFirstOrNull()?.let { + .awaitFirst().let { PersistedAggregate( aggregate = Json.decodeFromString(it.aggregate.data()), metaData = PersistenceMetaData( @@ -33,11 +33,7 @@ class JooqClientAggregateRepository( ) ) } - - override suspend fun get(id: Client.Id): PersistedAggregate = getOrThrow(id) { NoSuchElementException() } - - override suspend fun getOrThrow(id: Client.Id, block: () -> Throwable): PersistedAggregate = - find(id) ?: throw block() + } override suspend fun exists(id: Client.Id): Boolean = dsl.selectOne().from(CLIENTS).where(CLIENTS.ID.eq(id.value)).awaitFirstOrNull() != null diff --git a/acme-data/acme-data-scheduling/src/main/kotlin/com/acme/scheduling/data/JooqPracticeAggregateRepository.kt b/acme-data/acme-data-scheduling/src/main/kotlin/com/acme/scheduling/data/JooqPracticeAggregateRepository.kt index 9d3f72b..f38e972 100644 --- a/acme-data/acme-data-scheduling/src/main/kotlin/com/acme/scheduling/data/JooqPracticeAggregateRepository.kt +++ b/acme-data/acme-data-scheduling/src/main/kotlin/com/acme/scheduling/data/JooqPracticeAggregateRepository.kt @@ -20,24 +20,21 @@ class JooqPracticeAggregateRepository( private val clock: Clock = Clock.systemUTC() ) : AggregateRepository { - override suspend fun find(id: Practice.Id): PersistedAggregate? = - dsl.selectFrom(PRACTICES) - .where(PRACTICES.ID.eq(id.value)) - .awaitFirstOrNull()?.let { - PersistedAggregate( - aggregate = Json.decodeFromString(it.aggregate.data()), - metaData = PersistenceMetaData( - createdAt = it.createdAt, - updatedAt = it.updatedAt, - revision = it.revision, + override suspend fun findById(id: Practice.Id): Result> = + runCatching { + dsl.selectFrom(PRACTICES) + .where(PRACTICES.ID.eq(id.value)) + .awaitFirst().let { + PersistedAggregate( + aggregate = Json.decodeFromString(it.aggregate.data()), + metaData = PersistenceMetaData( + createdAt = it.createdAt, + updatedAt = it.updatedAt, + revision = it.revision, + ) ) - ) - } - - override suspend fun get(id: Practice.Id): PersistedAggregate = getOrThrow(id) { NoSuchElementException() } - - override suspend fun getOrThrow(id: Practice.Id, block: () -> Throwable): PersistedAggregate = - find(id) ?: throw block() + } + } override suspend fun exists(id: Practice.Id): Boolean = dsl.selectOne().from(PRACTICES).where(PRACTICES.ID.eq(id.value)).awaitFirstOrNull() != null diff --git a/acme-data/acme-data-scheduling/src/main/kotlin/com/acme/scheduling/data/JooqPractitionerAggregateRepository.kt b/acme-data/acme-data-scheduling/src/main/kotlin/com/acme/scheduling/data/JooqPractitionerAggregateRepository.kt index f0d3f15..74a50c5 100644 --- a/acme-data/acme-data-scheduling/src/main/kotlin/com/acme/scheduling/data/JooqPractitionerAggregateRepository.kt +++ b/acme-data/acme-data-scheduling/src/main/kotlin/com/acme/scheduling/data/JooqPractitionerAggregateRepository.kt @@ -20,25 +20,21 @@ class JooqPractitionerAggregateRepository( private val clock: Clock = Clock.systemUTC() ) : AggregateRepository { - override suspend fun find(id: Practitioner.Id): PersistedAggregate? = - dsl.selectFrom(PRACTITIONERS) - .where(PRACTITIONERS.ID.eq(id.value)) - .awaitFirstOrNull()?.let { - PersistedAggregate( - aggregate = Json.decodeFromString(it.aggregate.data()), - metaData = PersistenceMetaData( - createdAt = it.createdAt, - updatedAt = it.updatedAt, - revision = it.revision, + override suspend fun findById(id: Practitioner.Id): Result> = + runCatching { + dsl.selectFrom(PRACTITIONERS) + .where(PRACTITIONERS.ID.eq(id.value)) + .awaitFirst().let { + PersistedAggregate( + aggregate = Json.decodeFromString(it.aggregate.data()), + metaData = PersistenceMetaData( + createdAt = it.createdAt, + updatedAt = it.updatedAt, + revision = it.revision, + ) ) - ) - } - - override suspend fun get(id: Practitioner.Id): PersistedAggregate = - getOrThrow(id) { NoSuchElementException() } - - override suspend fun getOrThrow(id: Practitioner.Id, block: () -> Throwable): PersistedAggregate = - find(id) ?: throw block() + } + } override suspend fun exists(id: Practitioner.Id): Boolean = dsl.selectOne().from(PRACTITIONERS).where(PRACTITIONERS.ID.eq(id.value)).awaitFirstOrNull() != null diff --git a/acme-data/acme-data-scheduling/src/test/kotlin/com/acme/scheduling/data/JooqAppointmentAggregateRepositoryTest.kt b/acme-data/acme-data-scheduling/src/test/kotlin/com/acme/scheduling/data/JooqAppointmentAggregateRepositoryTest.kt index e92c853..2edd3a3 100644 --- a/acme-data/acme-data-scheduling/src/test/kotlin/com/acme/scheduling/data/JooqAppointmentAggregateRepositoryTest.kt +++ b/acme-data/acme-data-scheduling/src/test/kotlin/com/acme/scheduling/data/JooqAppointmentAggregateRepositoryTest.kt @@ -35,11 +35,13 @@ class JooqAppointmentAggregateRepositoryTest : ShouldSpec({ repo.save(appointment) repo.exists(appointment.id).shouldBeTrue() - val persistedAppointment = repo.get(appointment.id) + val persistedAppointment = repo.findById(appointment.id).getOrThrow() persistedAppointment.aggregate.shouldBe(appointment) - persistedAppointment.metaData.revision.shouldBe(1) - persistedAppointment.metaData.createdAt.shouldBe(time.now) - persistedAppointment.metaData.updatedAt.shouldBe(time.now) + with(persistedAppointment.metaData) { + revision.shouldBe(1) + createdAt.shouldBe(time.now) + updatedAt.shouldBe(time.now) + } } } @@ -59,11 +61,13 @@ class JooqAppointmentAggregateRepositoryTest : ShouldSpec({ ) updateRepo.save(expectedAppointment) - val persistedAppointment = updateRepo.get(appointment.id) + val persistedAppointment = updateRepo.findById(appointment.id).getOrThrow() persistedAppointment.aggregate.shouldBe(expectedAppointment) - persistedAppointment.metaData.revision.shouldBe(2) - persistedAppointment.metaData.createdAt.shouldBe(createTime.now) - persistedAppointment.metaData.updatedAt.shouldBe(updateTime.now) + with(persistedAppointment.metaData) { + revision.shouldBe(2) + createdAt.shouldBe(createTime.now) + updatedAt.shouldBe(updateTime.now) + } } } @@ -71,18 +75,7 @@ class JooqAppointmentAggregateRepositoryTest : ShouldSpec({ testTransaction { val repo = JooqAppointmentAggregateRepository(it.dsl()) shouldThrow { - repo.get(appointment.id) - } - } - } - - should("throw user supplied exception") { - testTransaction { - val repo = JooqAppointmentAggregateRepository(it.dsl()) - shouldThrow { - repo.getOrThrow(appointment.id) { - FakeException() - } + repo.findById(appointment.id).getOrThrow() } } } diff --git a/acme-data/acme-data-scheduling/src/test/kotlin/com/acme/scheduling/data/JooqClientAggregateRepositoryTest.kt b/acme-data/acme-data-scheduling/src/test/kotlin/com/acme/scheduling/data/JooqClientAggregateRepositoryTest.kt index 337cd6c..763ec04 100644 --- a/acme-data/acme-data-scheduling/src/test/kotlin/com/acme/scheduling/data/JooqClientAggregateRepositoryTest.kt +++ b/acme-data/acme-data-scheduling/src/test/kotlin/com/acme/scheduling/data/JooqClientAggregateRepositoryTest.kt @@ -33,11 +33,13 @@ class JooqClientAggregateRepositoryTest : ShouldSpec({ repo.save(client) repo.exists(client.id).shouldBeTrue() - val persistedClient = repo.get(client.id) + val persistedClient = repo.findById(client.id).getOrThrow() persistedClient.aggregate.shouldBe(client) - persistedClient.metaData.revision.shouldBe(1) - persistedClient.metaData.createdAt.shouldBe(time.now) - persistedClient.metaData.updatedAt.shouldBe(time.now) + with(persistedClient.metaData) { + revision.shouldBe(1) + createdAt.shouldBe(time.now) + updatedAt.shouldBe(time.now) + } } } @@ -52,11 +54,13 @@ class JooqClientAggregateRepositoryTest : ShouldSpec({ val expectedClient = client.copy(gender = Gender.FEMALE) updateRepo.save(expectedClient) - val persistedClient = updateRepo.get(client.id) + val persistedClient = updateRepo.findById(client.id).getOrThrow() persistedClient.aggregate.shouldBe(expectedClient) - persistedClient.metaData.revision.shouldBe(2) - persistedClient.metaData.createdAt.shouldBe(createTime.now) - persistedClient.metaData.updatedAt.shouldBe(updateTime.now) + with(persistedClient.metaData) { + revision.shouldBe(2) + createdAt.shouldBe(createTime.now) + updatedAt.shouldBe(updateTime.now) + } } } @@ -64,19 +68,9 @@ class JooqClientAggregateRepositoryTest : ShouldSpec({ testTransaction { val repo = JooqClientAggregateRepository(it.dsl()) shouldThrow { - repo.get(client.id) + repo.findById(client.id).getOrThrow() } } } - should("should throw user supplied exception") { - testTransaction { - val repo = JooqClientAggregateRepository(it.dsl()) - shouldThrow { - repo.getOrThrow(client.id) { - FakeException() - } - } - } - } }) diff --git a/acme-data/acme-data-scheduling/src/test/kotlin/com/acme/scheduling/data/JooqPracticeAggregateRepositoryTest.kt b/acme-data/acme-data-scheduling/src/test/kotlin/com/acme/scheduling/data/JooqPracticeAggregateRepositoryTest.kt index 8be60f0..f06c51f 100644 --- a/acme-data/acme-data-scheduling/src/test/kotlin/com/acme/scheduling/data/JooqPracticeAggregateRepositoryTest.kt +++ b/acme-data/acme-data-scheduling/src/test/kotlin/com/acme/scheduling/data/JooqPracticeAggregateRepositoryTest.kt @@ -32,7 +32,7 @@ class JooqPracticeAggregateRepositoryTest : ShouldSpec({ repo.save(practice) repo.exists(practice.id).shouldBeTrue() - val persistedPractice = repo.get(practice.id) + val persistedPractice = repo.findById(practice.id).getOrThrow() persistedPractice.aggregate.shouldBe(practice) persistedPractice.metaData.revision.shouldBe(1) persistedPractice.metaData.createdAt.shouldBe(time.now) @@ -43,8 +43,8 @@ class JooqPracticeAggregateRepositoryTest : ShouldSpec({ should("update an existing aggregate and increment revision") { testTransaction { val createTime = timeFixtureFactory() - val createRepo = JooqPracticeAggregateRepository(it.dsl(), createTime.clock) - createRepo.save(practice) + val repo = JooqPracticeAggregateRepository(it.dsl(), createTime.clock) + repo.save(practice) val updateTime = timeFixtureFactory() val updateRepo = JooqPracticeAggregateRepository(it.dsl(), updateTime.clock) @@ -53,11 +53,13 @@ class JooqPracticeAggregateRepositoryTest : ShouldSpec({ ) updateRepo.save(expectedPractice) - val persistedPractice = createRepo.get(practice.id) + val persistedPractice = repo.findById(practice.id).getOrThrow() persistedPractice.aggregate.shouldBe(expectedPractice) - persistedPractice.metaData.revision.shouldBe(2) - persistedPractice.metaData.createdAt.shouldBe(createTime.now) - persistedPractice.metaData.updatedAt.shouldBe(updateTime.now) + with(persistedPractice.metaData) { + revision.shouldBe(2) + createdAt.shouldBe(createTime.now) + updatedAt.shouldBe(updateTime.now) + } } } @@ -65,18 +67,7 @@ class JooqPracticeAggregateRepositoryTest : ShouldSpec({ testTransaction { val repo = JooqPracticeAggregateRepository(it.dsl()) shouldThrow { - repo.get(practice.id) - } - } - } - - should("throw user supplied exception") { - testTransaction { - val repo = JooqPracticeAggregateRepository(it.dsl()) - shouldThrow { - repo.getOrThrow(practice.id) { - FakeException() - } + repo.findById(practice.id).getOrThrow() } } } diff --git a/acme-data/acme-data-scheduling/src/test/kotlin/com/acme/scheduling/data/JooqPractitionerAggregateRepositoryTest.kt b/acme-data/acme-data-scheduling/src/test/kotlin/com/acme/scheduling/data/JooqPractitionerAggregateRepositoryTest.kt index 9ab5701..61913c3 100644 --- a/acme-data/acme-data-scheduling/src/test/kotlin/com/acme/scheduling/data/JooqPractitionerAggregateRepositoryTest.kt +++ b/acme-data/acme-data-scheduling/src/test/kotlin/com/acme/scheduling/data/JooqPractitionerAggregateRepositoryTest.kt @@ -35,11 +35,13 @@ class JooqPractitionerAggregateRepositoryTest : ShouldSpec({ repo.save(practitioner) repo.exists(practitioner.id).shouldBeTrue() - val persistedPractitioner = repo.get(practitioner.id) + val persistedPractitioner = repo.findById(practitioner.id).getOrThrow() persistedPractitioner.aggregate.shouldBe(practitioner) - persistedPractitioner.metaData.revision.shouldBe(1) - persistedPractitioner.metaData.createdAt.shouldBe(time.now) - persistedPractitioner.metaData.updatedAt.shouldBe(time.now) + with(persistedPractitioner.metaData) { + revision.shouldBe(1) + createdAt.shouldBe(time.now) + updatedAt.shouldBe(time.now) + } } } @@ -54,11 +56,13 @@ class JooqPractitionerAggregateRepositoryTest : ShouldSpec({ val expectedPractitioner = practitioner.copy(gender = Gender.FEMALE) updateRepo.save(expectedPractitioner) - val persistedPractitioner = createRepo.get(practitioner.id) + val persistedPractitioner = createRepo.findById(practitioner.id).getOrThrow() persistedPractitioner.aggregate.shouldBe(expectedPractitioner) - persistedPractitioner.metaData.revision.shouldBe(2) - persistedPractitioner.metaData.createdAt.shouldBe(createTime.now) - persistedPractitioner.metaData.updatedAt.shouldBe(updateTime.now) + with(persistedPractitioner.metaData) { + revision.shouldBe(2) + createdAt.shouldBe(createTime.now) + updatedAt.shouldBe(updateTime.now) + } } } @@ -66,18 +70,7 @@ class JooqPractitionerAggregateRepositoryTest : ShouldSpec({ testTransaction { val repo = JooqPractitionerAggregateRepository(it.dsl()) shouldThrow { - repo.get(practitioner.id) - } - } - } - - should("throw user supplied exception") { - testTransaction { - val repo = JooqPractitionerAggregateRepository(it.dsl()) - shouldThrow { - repo.getOrThrow(practitioner.id) { - FakeException() - } + repo.findById(practitioner.id).getOrThrow() } } } diff --git a/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/AggregateRepository.kt b/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/AggregateRepository.kt index f1f8af4..cf335cc 100644 --- a/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/AggregateRepository.kt +++ b/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/AggregateRepository.kt @@ -1,9 +1,7 @@ package com.acme.core interface AggregateRepository, I> { - suspend fun find(id: I): PersistedAggregate? - suspend fun get(id: I): PersistedAggregate - suspend fun getOrThrow(id: I, block: () -> Throwable): PersistedAggregate + suspend fun findById(id: I): Result> suspend fun exists(id: I): Boolean suspend fun save(aggregate: T) } diff --git a/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/CommandValidationException.kt b/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/CommandValidationException.kt index 73dfd66..35228f4 100644 --- a/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/CommandValidationException.kt +++ b/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/CommandValidationException.kt @@ -8,16 +8,16 @@ class CommandValidationException( ) : IllegalArgumentException("Invalid command") { constructor(command: Any, error: CommandValidationError) : this(command, setOf(error)) +} - interface CommandValidationError { - val message: String - } +interface CommandValidationError { + val message: String +} - data class InvalidAggregateReferenceError( - val fieldName: String, - val value: String, - override val message: String, - ) : CommandValidationError { - constructor(property: KProperty, value: String, message: String) : this(property.name, value, message) - } +data class InvalidAggregateReferenceError( + val fieldName: String, + val value: String, + override val message: String, +) : CommandValidationError { + constructor(property: KProperty, value: String, message: String) : this(property.name, value, message) } diff --git a/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/DefaultMessageBus.kt b/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/DefaultMessageBus.kt index 2dfc2d9..015a5e7 100644 --- a/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/DefaultMessageBus.kt +++ b/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/DefaultMessageBus.kt @@ -5,8 +5,8 @@ import kotlin.reflect.KClass import kotlin.reflect.KSuspendFunction2 class DefaultMessageBus( - private val commandHandlers: MutableMap, KSuspendFunction2> = mutableMapOf(), - private val eventHandlers: MutableMap, List>> = mutableMapOf() + private val commandHandlers: MutableMap, KSuspendFunction2> = mutableMapOf(), + private val eventHandlers: MutableMap, List>> = mutableMapOf() ) : MessageBus { private val logger = KotlinLogging.logger {} @@ -14,14 +14,14 @@ class DefaultMessageBus( override fun copy() = DefaultMessageBus(commandHandlers, eventHandlers) @Suppress("UNCHECKED_CAST") - override fun addEventHandler(eventClass: KClass<*>, handler: Any): DefaultMessageBus { + override fun addEventHandler(eventClass: KClass, handler: Any): DefaultMessageBus { val handlers = eventHandlers.getOrDefault(eventClass, emptyList()) if (handlers.contains(handler)) throw RuntimeException("Event handler has already been registered") eventHandlers[eventClass] = handlers.plus(handler as KSuspendFunction2) return this } - override fun addEventHandler(vararg pairs: Pair, Any>): DefaultMessageBus { + override fun addEventHandler(vararg pairs: Pair, Any>): DefaultMessageBus { pairs.forEach { addEventHandler(it.first, it.second) } @@ -29,13 +29,13 @@ class DefaultMessageBus( } @Suppress("UNCHECKED_CAST") - override fun addCommandHandler(commandClass: KClass<*>, handler: Any): DefaultMessageBus { + override fun addCommandHandler(commandClass: KClass, handler: Any): DefaultMessageBus { if (commandHandlers.containsKey(commandClass)) throw RuntimeException("Command handler already exists") commandHandlers[commandClass] = handler as KSuspendFunction2 return this } - override fun addCommandHandler(vararg pairs: Pair, Any>): DefaultMessageBus { + override fun addCommandHandler(vararg pairs: Pair, Any>): DefaultMessageBus { pairs.forEach { addCommandHandler(it.first, it.second) } diff --git a/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/InMemoryAggregateRepository.kt b/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/InMemoryAggregateRepository.kt index 130d330..0a60923 100644 --- a/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/InMemoryAggregateRepository.kt +++ b/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/InMemoryAggregateRepository.kt @@ -9,13 +9,9 @@ open class InMemoryAggregateRepository, I>( private val objects: MutableMap> = mutableMapOf() - override suspend fun find(id: I): PersistedAggregate? = objects[id] - - override suspend fun get(id: I): PersistedAggregate = - getOrThrow(id) { NoSuchElementException() } - - override suspend fun getOrThrow(id: I, block: () -> Throwable): PersistedAggregate = - find(id) ?: throw block() + override suspend fun findById(id: I): Result> = runCatching { + objects.get(id) ?: throw NoSuchElementException() + } override suspend fun exists(id: I): Boolean = objects.containsKey(id) diff --git a/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/MessageBus.kt b/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/MessageBus.kt index 49f1595..c460b77 100644 --- a/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/MessageBus.kt +++ b/acme-domain/acme-domain-core/src/main/kotlin/com/acme/core/MessageBus.kt @@ -4,9 +4,9 @@ import kotlin.reflect.KClass interface MessageBus { fun copy(): DefaultMessageBus - fun addEventHandler(eventClass: KClass<*>, handler: Any): DefaultMessageBus - fun addEventHandler(vararg pairs: Pair, Any>): DefaultMessageBus - fun addCommandHandler(commandClass: KClass<*>, handler: Any): DefaultMessageBus - fun addCommandHandler(vararg pairs: Pair, Any>): DefaultMessageBus + fun addEventHandler(eventClass: KClass, handler: Any): DefaultMessageBus + fun addEventHandler(vararg pairs: Pair, Any>): DefaultMessageBus + fun addCommandHandler(commandClass: KClass, handler: Any): DefaultMessageBus + fun addCommandHandler(vararg pairs: Pair, Any>): DefaultMessageBus suspend fun handle(message: Message, unitOfWork: UnitOfWork) } diff --git a/acme-domain/acme-domain-core/src/test/kotlin/com/acme/core/InMemoryAggregateRepositoryTest.kt b/acme-domain/acme-domain-core/src/test/kotlin/com/acme/core/InMemoryAggregateRepositoryTest.kt index 99727f3..5f12bac 100644 --- a/acme-domain/acme-domain-core/src/test/kotlin/com/acme/core/InMemoryAggregateRepositoryTest.kt +++ b/acme-domain/acme-domain-core/src/test/kotlin/com/acme/core/InMemoryAggregateRepositoryTest.kt @@ -17,8 +17,7 @@ class InMemoryAggregateRepositoryTest : ShouldSpec({ val aggregate = FakeAggregate("id123") val repo = InMemoryAggregateRepository().apply { save(aggregate) } repo.exists(aggregate.id).shouldBeTrue() - val persistedAggregate = repo.get(aggregate.id) - persistedAggregate.aggregate.shouldBe(aggregate) + repo.findById(aggregate.id).getOrThrow().aggregate.shouldBe(aggregate) } should("should save new aggregate") { @@ -26,14 +25,13 @@ class InMemoryAggregateRepositoryTest : ShouldSpec({ val aggregate = FakeAggregate("id123") repo.save(aggregate) repo.exists(aggregate.id).shouldBeTrue() - val persistedAggregate = repo.get(aggregate.id) - persistedAggregate.aggregate.shouldBe(aggregate) + repo.findById(aggregate.id).getOrThrow().aggregate.shouldBe(aggregate) } - should("throw user supplied exception") { + should("result in NoSuchElementException") { val repo = InMemoryAggregateRepository() - shouldThrow { - repo.getOrThrow("id123") { FakeException() } + shouldThrow { + repo.findById("id123").getOrThrow() } } }) diff --git a/acme-domain/acme-domain-scheduling/src/main/kotlin/com/acme/scheduling/handlers.kt b/acme-domain/acme-domain-scheduling/src/main/kotlin/com/acme/scheduling/handlers.kt index 9c6e68a..1ba6c9a 100644 --- a/acme-domain/acme-domain-scheduling/src/main/kotlin/com/acme/scheduling/handlers.kt +++ b/acme-domain/acme-domain-scheduling/src/main/kotlin/com/acme/scheduling/handlers.kt @@ -2,6 +2,7 @@ package com.acme.scheduling import com.acme.core.CommandValidationException import com.acme.core.DefaultMessageBus +import com.acme.core.InvalidAggregateReferenceError suspend fun createPractice(command: CreatePracticeCommand, uow: SchedulingUnitOfWork) { Practice( @@ -9,13 +10,10 @@ suspend fun createPractice(command: CreatePracticeCommand, uow: SchedulingUnitOf owner = command.owner, name = command.name, contactPoints = command.contactPoints - ) - .also { - uow.repositories.practices.save(it) - } - .also { - uow.addEvent(PracticeCreatedEvent(it)) - } + ).also { + uow.repositories.practices.save(it) + uow.addEvent(PracticeCreatedEvent(it)) + } } suspend fun createClient(command: CreateClientCommand, uow: SchedulingUnitOfWork) { @@ -24,10 +22,10 @@ suspend fun createClient(command: CreateClientCommand, uow: SchedulingUnitOfWork names = setOf(command.name), gender = command.gender, contactPoints = command.contactPoints - ).also { uow.repositories.clients.save(it) } - .also { - uow.addEvent(ClientCreatedEvent(it)) - } + ).also { + uow.repositories.clients.save(it) + uow.addEvent(ClientCreatedEvent(it)) + } } suspend fun createPractitioner(command: CreatePractitionerCommand, uow: SchedulingUnitOfWork) { @@ -37,46 +35,43 @@ suspend fun createPractitioner(command: CreatePractitionerCommand, uow: Scheduli gender = command.gender, names = setOf(command.name), contactPoints = command.contactPoints - ).also { uow.repositories.practitioners.save(it) } - .also { - uow.addEvent(PractitionerCreatedEvent(it)) - } + ).also { + uow.repositories.practitioners.save(it) + uow.addEvent(PractitionerCreatedEvent(it)) + } } suspend fun createAppointment(command: CreateAppointmentCommand, uow: SchedulingUnitOfWork) { - val errors = mutableSetOf() - if (!uow.repositories.practices.exists(command.practice)) { - errors.add( - CommandValidationException.InvalidAggregateReferenceError( + listOf( + uow.repositories.practices.exists(command.practice) to { + InvalidAggregateReferenceError( CreateAppointmentCommand::practice, command.practice.value, "Invalid practice" ) - ) - } - - if (!uow.repositories.practitioners.exists(command.practitioner)) { - errors.add( - CommandValidationException.InvalidAggregateReferenceError( + }, + uow.repositories.practitioners.exists(command.practitioner) to { + InvalidAggregateReferenceError( CreateAppointmentCommand::practitioner, command.practitioner.value, "Invalid practitioner" ) - ) - } - - if (!uow.repositories.clients.exists(command.client)) { - errors.add( - CommandValidationException.InvalidAggregateReferenceError( - CreateAppointmentCommand::client, - command.client.value, - "Invalid client" - ) - ) - } - - if (errors.size > 0) { - throw CommandValidationException(command, errors) + }, + uow.repositories.clients.exists(command.client) to { + InvalidAggregateReferenceError( + CreateAppointmentCommand::client, + command.client.value, + "Invalid client" + ) + } + ) + .filter { !it.first } + .map { it.second() } + .toSet() + .also { + if (it.isNotEmpty()) { + throw CommandValidationException(command, it) + } } Appointment( @@ -86,71 +81,87 @@ suspend fun createAppointment(command: CreateAppointmentCommand, uow: Scheduling practice = command.practice, state = command.state, period = command.period, - ) - .also { uow.repositories.appointments.save(it) } - .also { - uow.addEvent( - AppointmentCreatedEvent( - appointmentId = it.id, - clientId = it.client, - practitionerId = it.practitioner, - practiceId = it.practice, - period = it.period, - state = it.state, - ) + ).also { + uow.repositories.appointments.save(it) + uow.addEvent( + AppointmentCreatedEvent( + appointmentId = it.id, + clientId = it.client, + practitionerId = it.practitioner, + practiceId = it.practice, + period = it.period, + state = it.state, ) - } + ) + } } suspend fun markAppointmentAttended(command: MarkAppointmentAttendedCommand, uow: SchedulingUnitOfWork) { - uow.repositories.appointments.getOrThrow(command.appointment) { - CommandValidationException( - command, - CommandValidationException.InvalidAggregateReferenceError( - MarkAppointmentAttendedCommand::appointment, - command.appointment.value, - "Invalid appointment" - ) - ) - }.aggregate - .markAttended() - .also { uow.repositories.appointments.save(it) } - .also { - uow.addEvent(AppointmentAttendedEvent(it.id)) + uow.repositories.appointments.findById(command.appointment) + .onSuccess { + it.aggregate.markAttended().also { + uow.repositories.appointments.save(it) + uow.addEvent(AppointmentAttendedEvent(it.id)) + } + } + .onFailure { + when (it) { + is NoSuchElementException -> throw CommandValidationException( + command, + InvalidAggregateReferenceError( + MarkAppointmentAttendedCommand::appointment, + command.appointment.value, + "Invalid appointment" + ) + ) + else -> throw it + } } } suspend fun markAppointmentUnattended(command: MarkAppointmentUnattendedCommand, uow: SchedulingUnitOfWork) { - uow.repositories.appointments.getOrThrow(command.appointment) { - CommandValidationException( - command, - CommandValidationException.InvalidAggregateReferenceError( - MarkAppointmentAttendedCommand::appointment, - command.appointment.value, - "Invalid appointment" - ) - ) - }.aggregate.markUnattended() - .also { uow.repositories.appointments.save(it) } - .also { - uow.addEvent(AppointmentUnattendedEvent(it.id)) + uow.repositories.appointments.findById(command.appointment) + .onSuccess { + it.aggregate.markUnattended().also { + uow.repositories.appointments.save(it) + uow.addEvent(AppointmentUnattendedEvent(it.id)) + } + }.onFailure { + when (it) { + is NoSuchElementException -> throw CommandValidationException( + command, + InvalidAggregateReferenceError( + MarkAppointmentAttendedCommand::appointment, + command.appointment.value, + "Invalid appointment" + ) + ) + else -> throw it + } } } suspend fun cancelAppointment(command: CancelAppointmentCommand, uow: SchedulingUnitOfWork) { - uow.repositories.appointments.getOrThrow(command.appointment) { - CommandValidationException( - command, - CommandValidationException.InvalidAggregateReferenceError( - MarkAppointmentAttendedCommand::appointment, - command.appointment.value, - "Invalid appointment" - ) - ) - }.aggregate.cancel() - .also { uow.repositories.appointments.save(it) } - .also { - uow.addEvent(AppointmentCanceledEvent(it.id)) + uow.repositories.appointments.findById(command.appointment) + .onSuccess { + it.aggregate.cancel().also { + uow.repositories.appointments.save(it) + uow.addEvent(AppointmentCanceledEvent(it.id)) + } + } + .onFailure { + when (it) { + is NoSuchElementException -> throw CommandValidationException( + command, + InvalidAggregateReferenceError( + MarkAppointmentAttendedCommand::appointment, + command.appointment.value, + "Invalid appointment" + ) + ) + else -> throw it + } + } } diff --git a/acme-domain/acme-domain-scheduling/src/test/kotlin/com/acme/scheduling/CommandHandlerTest.kt b/acme-domain/acme-domain-scheduling/src/test/kotlin/com/acme/scheduling/CommandHandlerTest.kt index b7b1689..8ae39b5 100644 --- a/acme-domain/acme-domain-scheduling/src/test/kotlin/com/acme/scheduling/CommandHandlerTest.kt +++ b/acme-domain/acme-domain-scheduling/src/test/kotlin/com/acme/scheduling/CommandHandlerTest.kt @@ -1,6 +1,9 @@ package com.acme.scheduling import com.acme.core.CommandValidationException +import com.acme.core.InMemoryAggregateRepository +import com.acme.core.InvalidAggregateReferenceError +import com.acme.core.PersistedAggregate import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.ShouldSpec import io.kotest.matchers.booleans.shouldBeTrue @@ -213,7 +216,7 @@ class CommandHandlerTest : ShouldSpec({ } context("markAppointmentAttended") { - should("should update aggregate and publish event") { + should("update aggregate and publish event") { val appointment = Appointment( id = Appointment.Id("Appointment123"), practice = Practice.Id("Practice123"), @@ -234,7 +237,7 @@ class CommandHandlerTest : ShouldSpec({ ) } - should("markAppointmentAttended should throw exception when aggregate does not exist") { + should("throw CommandValidationException when aggregate does not exist") { val cmd = MarkAppointmentAttendedCommand(Appointment.Id("Appointment123")) val uow = InMemorySchedulingUnitOfWork() @@ -245,14 +248,23 @@ class CommandHandlerTest : ShouldSpec({ exc.command.shouldBe(cmd) exc.errors.shouldBe( setOf( - CommandValidationException.InvalidAggregateReferenceError( + InvalidAggregateReferenceError( MarkAppointmentAttendedCommand::appointment, - cmd.appointment.value, + "Appointment123", "Invalid appointment" ) ) ) } + + should("throw unexpected exception") { + val cmd = MarkAppointmentAttendedCommand(Appointment.Id("Appointment123")) + val uow = InMemorySchedulingUnitOfWork(appointments = FakeAppointmentInMemoryAggregateRepository()) + + shouldThrow { + markAppointmentAttended(cmd, uow) + } + } } context("markAppointmentUnattended") { @@ -277,7 +289,7 @@ class CommandHandlerTest : ShouldSpec({ ) } - should("throw exception when aggregate does not exist") { + should("throw CommandValidationException when aggregate does not exist") { val cmd = MarkAppointmentUnattendedCommand(Appointment.Id("Appointment123")) val uow = InMemorySchedulingUnitOfWork() @@ -288,14 +300,23 @@ class CommandHandlerTest : ShouldSpec({ exc.command.shouldBe(cmd) exc.errors.shouldBe( setOf( - CommandValidationException.InvalidAggregateReferenceError( + InvalidAggregateReferenceError( MarkAppointmentAttendedCommand::appointment, - cmd.appointment.value, + "Appointment123", "Invalid appointment" ) ) ) } + + should("throw unexpected exception") { + val cmd = MarkAppointmentUnattendedCommand(Appointment.Id("Appointment123")) + val uow = InMemorySchedulingUnitOfWork(appointments = FakeAppointmentInMemoryAggregateRepository()) + + shouldThrow { + markAppointmentUnattended(cmd, uow) + } + } } context("cancelAppointment") { @@ -319,25 +340,40 @@ class CommandHandlerTest : ShouldSpec({ listOf(AppointmentCanceledEvent(appointment.id)) ) } - } - should("throw exception when aggregate does not exist") { - val cmd = CancelAppointmentCommand(Appointment.Id("Appointment123")) - val uow = InMemorySchedulingUnitOfWork() + should("throw CommandValidationException when aggregate does not exist") { + val cmd = CancelAppointmentCommand(Appointment.Id("Appointment123")) + val uow = InMemorySchedulingUnitOfWork() - val exc = shouldThrow { - cancelAppointment(cmd, uow) - } + val exc = shouldThrow { + cancelAppointment(cmd, uow) + } - exc.command.shouldBe(cmd) - exc.errors.shouldBe( - setOf( - CommandValidationException.InvalidAggregateReferenceError( - MarkAppointmentAttendedCommand::appointment, - cmd.appointment.value, - "Invalid appointment" + exc.command.shouldBe(cmd) + exc.errors.shouldBe( + setOf( + InvalidAggregateReferenceError( + MarkAppointmentAttendedCommand::appointment, + "Appointment123", + "Invalid appointment" + ) ) ) - ) + } + + should("throw unexpected exception") { + val cmd = CancelAppointmentCommand(Appointment.Id("Appointment123")) + val uow = InMemorySchedulingUnitOfWork(appointments = FakeAppointmentInMemoryAggregateRepository()) + + shouldThrow { + cancelAppointment(cmd, uow) + } + } } }) + +class FakeAppointmentInMemoryAggregateRepository : InMemoryAggregateRepository() { + override suspend fun findById(id: Appointment.Id): Result> { + return Result.failure(NotImplementedError()) + } +} diff --git a/acme-domain/acme-domain-scheduling/src/test/kotlin/com/acme/scheduling/InMemorySchedulingUnitOfWork.kt b/acme-domain/acme-domain-scheduling/src/test/kotlin/com/acme/scheduling/InMemorySchedulingUnitOfWork.kt index 910bc1e..f21deff 100644 --- a/acme-domain/acme-domain-scheduling/src/test/kotlin/com/acme/scheduling/InMemorySchedulingUnitOfWork.kt +++ b/acme-domain/acme-domain-scheduling/src/test/kotlin/com/acme/scheduling/InMemorySchedulingUnitOfWork.kt @@ -4,13 +4,18 @@ import com.acme.core.AbstractUnitOfWork import com.acme.core.Event import com.acme.core.InMemoryAggregateRepository -class InMemorySchedulingUnitOfWork : AbstractUnitOfWork(), SchedulingUnitOfWork { +class InMemorySchedulingUnitOfWork( + appointments: InMemoryAggregateRepository = InMemoryAggregateRepository(), + clients: InMemoryAggregateRepository = InMemoryAggregateRepository(), + practitioners: InMemoryAggregateRepository = InMemoryAggregateRepository(), + practices: InMemoryAggregateRepository = InMemoryAggregateRepository() +) : AbstractUnitOfWork(), SchedulingUnitOfWork { override val repositories = object : SchedulingPersistenceModule { - override val appointments = InMemoryAggregateRepository() - override val clients = InMemoryAggregateRepository() - override val practitioners = InMemoryAggregateRepository() - override val practices = InMemoryAggregateRepository() + override val appointments = appointments + override val clients = clients + override val practitioners = practitioners + override val practices = practices } private var _events: MutableList = mutableListOf() diff --git a/acme-web/acme-web-api/src/main/kotlin/com/acme/web/api/errorhandlers.kt b/acme-web/acme-web-api/src/main/kotlin/com/acme/web/api/errorhandlers.kt index 257ac13..cfdd901 100644 --- a/acme-web/acme-web-api/src/main/kotlin/com/acme/web/api/errorhandlers.kt +++ b/acme-web/acme-web-api/src/main/kotlin/com/acme/web/api/errorhandlers.kt @@ -1,6 +1,7 @@ package com.acme.web.api import com.acme.core.CommandValidationException +import com.acme.core.InvalidAggregateReferenceError import com.acme.ktor.server.logging.logger import com.acme.ktor.server.validation.RequestBodyValidationException import com.acme.ktor.server.validation.RequestDecodingException @@ -28,7 +29,7 @@ suspend fun ApplicationCall.onCommandValidationException(json: Json, exc: Comman VndError( message = it.message, path = when (it) { - is CommandValidationException.InvalidAggregateReferenceError -> "/${it.fieldName}" + is InvalidAggregateReferenceError -> "/${it.fieldName}" else -> null } ) diff --git a/acme-web/acme-web-api/src/main/kotlin/com/acme/web/api/scheduling/commands.kt b/acme-web/acme-web-api/src/main/kotlin/com/acme/web/api/scheduling/commands.kt index 4774b29..cdb6c17 100644 --- a/acme-web/acme-web-api/src/main/kotlin/com/acme/web/api/scheduling/commands.kt +++ b/acme-web/acme-web-api/src/main/kotlin/com/acme/web/api/scheduling/commands.kt @@ -37,7 +37,7 @@ fun Route.schedulingCommands( post { val principal = call.authenticatedUser() val command = call.receiveAndValidate() - .toCommand(defaultIdGenerator(), principal) + .toCommand(defaultIdGenerator(), WebContext(principal)) val resourceHref = href(PracticeResourceLocation(command.id.value)) unitOfWork.transaction { @@ -56,7 +56,7 @@ fun Route.schedulingCommands( post { val principal = call.authenticatedUser() val command = call.receiveAndValidate() - .toCommand(defaultIdGenerator(), principal) + .toCommand(defaultIdGenerator(), WebContext(principal)) val resourceHref = href(ClientResourceLocation(command.id.value)) unitOfWork.transaction { @@ -75,7 +75,7 @@ fun Route.schedulingCommands( post { val principal = call.authenticatedUser() val command = call.receiveAndValidate() - .toCommand(defaultIdGenerator(), principal) + .toCommand(defaultIdGenerator(), WebContext(principal)) val resourceHref = href(PractitionerResourceLocation(command.id.value)) unitOfWork.transaction { @@ -94,7 +94,7 @@ fun Route.schedulingCommands( post { val principal = call.authenticatedUser() val command = call.receiveAndValidate() - .toCommand(defaultIdGenerator(), principal) + .toCommand(defaultIdGenerator(), WebContext(principal)) val resourceHref = href(AppointmentResourceLocation(command.id.value)) unitOfWork.transaction { diff --git a/acme-web/acme-web-api/src/main/kotlin/com/acme/web/api/scheduling/mappers.kt b/acme-web/acme-web-api/src/main/kotlin/com/acme/web/api/scheduling/mappers.kt index fcb39b3..b12a5d5 100644 --- a/acme-web/acme-web-api/src/main/kotlin/com/acme/web/api/scheduling/mappers.kt +++ b/acme-web/acme-web-api/src/main/kotlin/com/acme/web/api/scheduling/mappers.kt @@ -1,5 +1,6 @@ package com.acme.web.api.scheduling +import com.acme.core.Command import com.acme.scheduling.Appointment import com.acme.scheduling.AppointmentState import com.acme.scheduling.Client @@ -34,7 +35,11 @@ fun Period.toValueObject() = com.acme.scheduling.Period.Unknown } -fun CreateAppointmentCommandRequest.toCommand(id: String, authenticatedUser: AcmeWebUserPrincipal) = +data class WebContext( + val authenticatedUser: AcmeWebUserPrincipal +) + +fun CreateAppointmentCommandRequest.toCommand(id: String, ctx: WebContext) = CreateAppointmentCommand( id = Appointment.Id(id), practitioner = Practitioner.Id(practitionerId!!), @@ -42,11 +47,9 @@ fun CreateAppointmentCommandRequest.toCommand(id: String, authenticatedUser: Acm practice = Practice.Id(practiceId!!), state = AppointmentState.valueOf(state!!), period = com.acme.scheduling.Period.Bounded(from!!, to!!), - ).apply { - metadata.set("principal" to authenticatedUser) - } + ).applyWebMetaData(ctx) -fun CreateClientCommandRequest.toCommand(id: String, authenticatedUser: AcmeWebUserPrincipal) = +fun CreateClientCommandRequest.toCommand(id: String, ctx: WebContext) = CreateClientCommand( id = Client.Id(id), name = name!!.toValueObject(), @@ -58,14 +61,12 @@ fun CreateClientCommandRequest.toCommand(id: String, authenticatedUser: AcmeWebU ContactPoint.Email.Unverified(it) } ).toSet() - ).apply { - metadata.set("principal" to authenticatedUser) - } + ).applyWebMetaData(ctx) -fun CreatePracticeCommandRequest.toCommand(id: String, authenticatedUser: AcmeWebUserPrincipal) = +fun CreatePracticeCommandRequest.toCommand(id: String, ctx: WebContext) = CreatePracticeCommand( id = Practice.Id(id), - owner = Practitioner.Id(authenticatedUser.id), + owner = Practitioner.Id(ctx.authenticatedUser.id), name = Practice.Name(name!!), contactPoints = phoneNumbers!!.map { ContactPoint.Phone.Unverified(it) @@ -74,14 +75,12 @@ fun CreatePracticeCommandRequest.toCommand(id: String, authenticatedUser: AcmeWe ContactPoint.Email.Unverified(it) } ).toSet() - ).apply { - metadata.set("principal" to authenticatedUser) - } + ).applyWebMetaData(ctx) -fun CreatePractitionerCommandRequest.toCommand(id: String, authenticatedUser: AcmeWebUserPrincipal) = +fun CreatePractitionerCommandRequest.toCommand(id: String, ctx: WebContext) = CreatePractitionerCommand( id = Practitioner.Id(id), - user = UserId(authenticatedUser.id), + user = UserId(ctx.authenticatedUser.id), name = name!!.toValueObject(), gender = Gender.valueOf(gender!!), contactPoints = phoneNumbers!!.map { @@ -91,6 +90,9 @@ fun CreatePractitionerCommandRequest.toCommand(id: String, authenticatedUser: Ac ContactPoint.Email.Unverified(it) } ).toSet() - ).apply { - metadata.set("principal" to authenticatedUser) - } + ).applyWebMetaData(ctx) + +fun C.applyWebMetaData(ctx: WebContext): C { + metadata.set("principal" to ctx.authenticatedUser) + return this +} diff --git a/acme-web/acme-web-api/src/test/kotlin/com/acme/web/api/scheduling/MessagesTest.kt b/acme-web/acme-web-api/src/test/kotlin/com/acme/web/api/scheduling/MessagesTest.kt index 9878e36..8b42d99 100644 --- a/acme-web/acme-web-api/src/test/kotlin/com/acme/web/api/scheduling/MessagesTest.kt +++ b/acme-web/acme-web-api/src/test/kotlin/com/acme/web/api/scheduling/MessagesTest.kt @@ -1,6 +1,7 @@ package com.acme.web.api.scheduling import com.acme.core.CommandValidationException +import com.acme.core.InvalidAggregateReferenceError import com.acme.scheduling.Appointment import com.acme.scheduling.AppointmentState import com.acme.scheduling.CancelAppointmentCommand @@ -226,17 +227,17 @@ class MessagesTest : ShouldSpec({ exc.command.shouldBe(command) exc.errors.shouldBe( setOf( - CommandValidationException.InvalidAggregateReferenceError( + InvalidAggregateReferenceError( fieldName = "client", value = "Client123", message = "Invalid client" ), - CommandValidationException.InvalidAggregateReferenceError( + InvalidAggregateReferenceError( fieldName = "practitioner", value = "Practitioner123", message = "Invalid practitioner" ), - CommandValidationException.InvalidAggregateReferenceError( + InvalidAggregateReferenceError( fieldName = "practice", value = "Practice123", message = "Invalid practice"