Building microservices through Event Driven Architecture part6: Implementing EventSourcing on Domain Model
This tutorial is the 6th part of a series : Building microservices through Event Driven Architecture.
The previous step is about docker using SQL Server Linux implementation : https://logcorner.com/building-microservices-through-event-driven-architecture-part5-dockerization-web-api-core-and-sql-server-linux/
During this journey, I will talk about Event Sourcing implementation on Domain Model.
The fundamental idea of Event Sourcing is that of ensuring every change to the state of an application is captured in an event object, and that these event objects are themselves stored in the sequence they were applied for the same lifetime as the application state itself.
Martin Fowler https://martinfowler.com/eaaDev/EventSourcing.html
So you have to store all business changes to the system instead of storing the current state. and you will rebuild the current state by applying all the events to the aggregate.
Eventsourcing and CQRS are architectural patterns , so you do not need a specific language or framework.
CQRS: Separate everything about writing or changes (command) from everything about reading of the state of the Domain Model (Query).
By separating these two logics, you can optimize them separately.
- A Command writes events into an eventstore, and these events should be replayed to rebuild the state of the aggregate.
- A Query request the ReadModel to get the current state.
In the DDD world , you can build a list of Domain Events : business changes happened in the past expressed in the Ubiquitous Language : ex ThePackageHasBeenDeliveredToCustomer.
Domain Events are immutable, when a event happen it cannot be changed.
So, to correct a mistake on an event, you have to create a compensation event with correct values like bank account transactions.
The aggregate record committed events and protect invariants. It is the transaction boundary. To deal with concurrency I will use Optimistic Concurrency Control (OCC) with versioning.
Without acquiring locks each transaction verifies that no other transaction has modified the data it has read. If not the transaction is committed, if yes the transaction rolls back and can be restarted.
With versionning , the user reads the current state of the aggregate, then send commands with the version number, if the version number match the version of the aggregate then the transaction is committed.
If the version number does not match the version of the aggregate, in this case it means that the data has been updated by someone else. So the user should read again the data to get the correct version and retry.
Optimistic concurrency control (OCC)
EVENT SOURCING INTERFACES
Let us define some interfaces IDomainEvent and IEventSourcing
EventId is the identifier of the event
- Version is the current version of the aggregate
- ValidateVersion is a function that should raise an concurrency exception if the provided version is not equals to the current version of the aggregate.
- ApplyEvent is a function that applies an event to the aggragate .
- GetUncommittedEvents : a function that lists the history of all the events (not yet persisted) of the aggregate
- ClearUncommittedEvents : a function that clear the list of uncommitted events of the aggregate
EVENT SOURCING IMPLEMENTATION
Let us use the AggregateRoot abstract class to implement IEventSourcing interface
Some tests fail because of this change
Let us implement my first test
VALIDATE VERSION IMPLEMENTATION
ValidateVersion should raise an concurrency exception if the given version is not equals to the current version of the aggregate
So the first test is ValidateVersionWithInvalidExpectedVersionShouldRaiseConcurrencyException
TEST CASE 1 : ValidateVersion With Invalid Expected Version Should Raise Concurrency Exception :
Let us define StubEventSourcing and ConcurrencyException class to fix build errors.
ValidateVersion is a method of an abstract class AggregateRoot , so to test it I can create a StubEventSourcing class that inherits from AggregateRoot.
Here is the final implementation of the test.
The test fail so let’s implement the method to make it succeed.
The test succeeded.
TEST CASE 2 : ValidateVersion With Valid Expected Version Then Expected Version Should Be Equals To Aggregate Version
Then Code Coverage of ValidateVersion is 100%.
APPLY EVENT IMPLEMENTATION
TEST CASE 3 : ApplyEvent Should Populate Aggregate Properties With Event Properties
To rebuild aggregate state, ApplyEvent will be called for each event.
So I verify that if I apply an event to an empty aggregateroot, then the properties of the event should be applied to the given aggregate.
I have to create an Event class with properties, Title, Url, Description ,Type and an empty aggregateroot, and verify that when applying this event, the equivalent properties of the aggregateroot are updated correctly.
The aggregateroot of our domain model is Speech Entity, so I can create en empty speech object like this.
Let us use our speechCreatedEvent.
So I can create a Event abstract class that implement IDomainEvent and every event should inherits from it.
- AggregateId is the identifier of the aggregateroot
- EventId is the identifier of the event
- OccurredOn is the the date on which the event occurred
Here is the final implementation of the test.
I can implement ApplyEvent like this :
If the list of uncommitted events does not contains the given event, then I apply this event to aggregate
- ((dynamic)this).Apply((dynamic)@event); with raise the following method
The test succeeded and codecoverage is OK.
GETUNCOMMITED EVENTS IMPLEMENTATION
TEST CASE 4 : GetUncommittedEvents Of New Aggregate Should Return List Of IDomainEvent
Here I have to verify that the GetUncommittedEvents method should return the list of uncommitted events.
For now I have not yet implement AddDomainEvent that add an event to the list of uncommitted events, so for this test I simply verify that the list of uncommitted events is empty and is of type IEnumerable<IDomainEvent>.
ADD DOMAIN EVENT IMPLEMENTATION
TEST CASE 5 : AddDomainEvent With Invalid Version Should Raise ConcurrencyException
AddDomainEvent belongs to Aggreroot class and is protected , so only aggregateroot entities can raise an event.
To test this method I can proceed as follow :
I create a StubEventSourcing class that implement a ExposeAddDomainEvent method to expose AddDomainEvent method.
Here is the final implementation of the test
I have updated the AddDomainEvent method , it takes now an event and a version.
Let us call ValidateVersion to make test succeed.
TEST CASE 6 : AddDomainEvent With Valid Version Then Version Of Event Should Be Equals To Current Version Of Aggregate
Here I have to verify that the version of the event equals to the current version of the aggregate, so I will create a method BuilVersion that sets the version of the event with correct version. and use @event.BuildVersion(_version + 1); on AddDomainEvent method.
Here is the event I use for tests.
BuilVersion set the aggregateVersion property of the event with the current aggregateroot version incremented by 1.
Test already fails because AggregateVersion is -1 and EventVersion is 0.
So I call ApplyEvent using event and new event version to make it pass.
Call of ApplyEvent.
Test fais because I need to implement ApplyEvent of type @event
So let us implement ApplyEvent(SubEvent subEvent)
And everything pass
TEST CASE 7 : AddDomainEvent With Valid Version Then Event Should Be Applied To Aggregate
Here I verify that when applying Event, then the event is applyed to the aggregate
TEST CASE 8: AddDomainEvent With Valid Version Then UncommittedEvents Should Be Single
Here I verufy that when I apply an event , then the event should be the only event that belongs to the list of uncommitted events.
CLEAR UNCOMMITED EVENTS IMPLEMENTATION
TEST CASE 9 : ClearUncommittedEvents Then Uncommitted Events Should Be Empty
Here I verify that when I clear the list of uncommitted events, then the list should be empty
Here is codecoverage.
FIX SOME TESTS ET CHECK CODE COVERAGE
I have to remove _mediaFileItems.Add(mediaFile); from CreateMedia(MediaFile mediaFile, int originalVersion) because it is now implemented below
Code coverage is Ok (99%).
source code of this article is available here (Feature/Task/EventSourcingCoreDomain)