This article was originally published at: https://www.ahmetkucukoglu.com/en/event-sourcing-with-asp-net-core-01-store/
1. Introduction
I recommend you to read the article below before applying this example tutorial.
https://www.ahmetkucukoglu.com/en/what-is-event-sourcing/
In the article I have mentioned above, I had formed a sentence as follows.
There is a technology called “Event Store” in the .NET world for Event Sourcing. This technology offers solutions for “Aggregate” and “Projection”. In other words, in addition to providing the store where we can record the events, it also provides the “Messaging” and “Projection” services, which are necessary for us to record in “Query” databases.
In this article, we will deal with the store section of the Event Store. In other words, we will deal with the database feature where we can save events. In the next article, we will deal with the messaging part.
As an sample application, I chose the classic Kanban Board sample.
Our RESTful API endpoints will be as follows.
[POST] api/tasks/{id}/create
[PATCH] api/tasks/{id}/assign
[PATCH] api/tasks/{id}/move
[PATCH] api/tasks/{id}/complete
Our events will be as follows.
CreatedTask : When Task is created, an event with this name will be created.
AssignedTask : An event with this name will be created when Task is assigned to someone.
MovedTask : When Task is moved to a section (In Progress, Done), an event with this name will be created.
CompletedTask : When Task is completed, an event with this name will be created.
2. Installing the Event Store
We run the Event Store in Docker with the following command line.
docker run -d --name eventstore -p 2113:2113 -p 1113:1113 eventstore/eventstore
When the Event Store run, you can enter the panel from the address below. The default username is "admin" and the password is "changeit".
3. Creating the API Project
We create an ASP.NET Core Web API Application project named "EventSourcingTaskApp". We install the "EventStore.Client" package to the project with the following command line.
dotnet add package EventStore.Client -v 5.0.6
We add Event Store connection information to appsettings.json as follows.
In the Startup.cs file, we connect the Event Store and add it to the DI Container.
4. Aggregate Base Class and Aggregate Repository
Let's add two folders named "Core" and "Infrastructure" to the project. We will add Task-related entities, events and exceptions to the Core folder. For the Infrastructure folder, we will add our repository class that will connect the Event Store.
4.1. Aggregate Base Class
Let's add a folder named "Framework" inside the Core folder and add a class named "Aggregate.cs" into it. Let's paste the code below into the class.
This is a standard class. Aggregates are derived from this base class. In our example, Task is an aggregate and this will be derived from the base class.
In the line 9, we define the variable where aggregate events will be stored.
In the line 11 we state that aggregate will have an Id.
In the line 12 we state that the default version of aggregate is "-1".
In the line 16, we write the method that will add events to the variable defined on the line 9.
In the line 23, we write the method that will apply the events to aggregate. The final version of aggregate will be created by running this method for each event read from the Event Store.
In the line 33, we write the method that returns the events on aggregate. While sending events to the Event Store, this method will be run and events will be received.
4.2. Aggregate Repository
Let's add a class named "AggregateRepository.cs" inside the Infrastructure folder. Let's paste the code below into the class.
This is also a standard class. We use this repository when sending events to the Event Store or receiving events from the Event Store.
4.2.1. Sending an Event (Append Events to Stream)
In the line 22, we take the events on aggregate and map them to the EventData class. Event Store stores events in the EventData type.
As the first parameter, it takes for the event's id.
As the second parameter, it takes for the name of the event. Example event name; CreatedTask, AssignedTask etc.
As the third parameter, it takes whether the event data is in json type.
As the fourth parameter, it takes the event data. Since it takes in byte array type, serialize and encoding processes are performed.
As the fifth parameter, it takes the metadata. This parameter can be passed as null but we pass the type of the event class as "fullname" so that we can use this class type when deserializing the events. Example fullname; EventSourcingTaskApp.Core.Events.CreatedTask.
In the line 36, we set the stream name. Event aggregates in the Event Store is called stream. So aggregate is expressed as a stream in Event Store. Example stream name; Task-518e7c15-36ac-4edb-8fa5-931fb8ffa3a5.
In the 38th line, events are recorded in the Event Store.
4.2.2. Reading an Event (Read Events from Stream)
In the line 47, we set the stream name.
In line 53, events are received from the Event Store in order according to the version numbers in the loop.
In the 58th line, the load method of aggregate is called and the events are applied to aggregate and the final form of aggregate is created.
Let's add AggregateRepository class to DI Container in the Startup.cs file as below.
5. Defining Task and Use Cases
In this part, we will start creating events, exceptions, and use cases related to Task.
5.1. Defining Task
Let's add an aggregate class named "Task.cs" in the Core folder. Let's paste the code below into the class.
Task will have the Title, Section, AssignedTo and IsCompleted.
Again, let's add a class named "BoardSections.cs" into Core. Let's paste the code below into the class.
5.2. Defining Exceptions
Let's add a folder named "Exceptions" inside the Core folder and add a class named "TaskAlreadyCreatedException.cs" in it. Let's paste the code below into the class. If it tries to create a task with the same id information, we will throw this error.
Let's add a class named "TaskCompletedException.cs" in the Exceptions folder inside the Core folder. Let's paste the code below into the class. If any process is tried on the completed task, we will throw this error.
Let's add a class named "TaskNotFoundException.cs" inside the Exceptions folder inside the Core folder. Let's paste the code below into the class. If Task is not found, we will throw this error.
Now that we have created the Task aggregate and exceptions, we can start writing use cases related to the Task.
5.3. Create Task
When Task is created; We will keep task id, its title and the information of who created the task, as event data.
Let's add a folder named "Events" inside the Core folder and add an event class named "CreatedTask.cs" into it. Let's paste the code below into the class.
Let's edit the Task.cs class as follows.
With the Create method, we store the CreatedTask event on aggregate, so that we can send these stored events to the Event Store.
In the line 19, we apply the CreatedTask event from the Event Store to aggregate.
5.4. Assign Task
When Task is assigned to someone; We will keep task id , information of who has assigned the task and to whom the task has been assigned as event data.
Let's add an event class named "AssignedTask.cs" to the Events folder inside the Core folder. Let's paste the code below into the class.
Let's edit the Task.cs class as follows.
With the Assign method, we store the AssignedTask event on aggregate so that we can send these stored events to the Event Store.
In the line 20, we apply AssignedTask event received from Event Store to aggregate.
5.5. Move Task
When Task is moved to "In Progress" or "Done" section; We will keep the task id, information of who has moved the task and to which part the task has been moved, as event data
Let's add an event class named "MovedTask.cs" to the Events folder inside the Core folder. Let's paste the code below into the class.
Let's edit the Task.cs class as follows.
We store the MovedTask event on aggregate with the Move method, so that we can send these stored events to the Event Store.
In the line 21, we apply the MovedTask event received from the Event Store to aggregate.
5.6. Complete Task
When Task is completed; We will keep task id, who has completed the task as event data.
Let's add an event class named "CompletedTask.cs" to the Events folder inside the Core folder. Let's paste the code below into the class.
Let's edit the Task.cs class as follows.
With the Complete method, we store the CompletedTask event on aggregate so that we can send these stored events to the Event Store.
In the line 22, we apply the CompletedTask event received from the Event Store to aggregate.
6. Preparing API Endpoints
Let's create a controller named "TasksController" and paste the code below.
As you can see in the actions, firstly, events are taken from the Event Store with the AggregateRepository's Load method and aggregate is created. Then, new events are stored in aggregate with the use case methods on aggregate. With the AggregateRepository’s Save method, these stored events are sent to the Event Store.
Let's run the API and send requests to the API.
Let's create a task with the curl command line below.
curl -d "title=Event Store kurulacak" -H "Content-Type: application/x-www-form-urlencoded" -X POST https://localhost:44361/api/tasks/3a7daba9-872c-4f4d-8d6f-e9700d78c4f5/create
Let's assign the task to someone with the curl command line below.
curl -d "assignedTo=Aziz CETİN" -H "Content-Type: application/x-www-form-urlencoded" -X PATCH https://localhost:44361/api/tasks/3a7daba9-872c-4f4d-8d6f-e9700d78c4f5/assign
Let's pull the task to In-Progress with the curl command line below.
curl -d "section=2" -H "Content-Type: application/x-www-form-urlencoded" -X PATCH https://localhost:44361/api/tasks/3a7daba9-872c-4f4d-8d6f-e9700d78c4f5/move
Let's pull the task to Done with the curl command line below.
curl -d "section=3" -H "Content-Type: application/x-www-form-urlencoded" -X PATCH https://localhost:44361/api/tasks/3a7daba9-872c-4f4d-8d6f-e9700d78c4f5/move
Let's complete the task with the curl command line below.
curl -d -H "Content-Type: application/x-www-form-urlencoded" -X PATCH https://localhost:44361/api/tasks/3a7daba9-872c-4f4d-8d6f-e9700d78c4f5/complete
You can check the stream by entering the Event Store panel.
http://localhost:2113/web/index.html#/streams/Task-3a7daba9-872c-4f4d-8d6f-e9700d78c4f5
You can access the final version of the project from Github.
Good luck.
Top comments (2)
Excellent example. Thank you for detailing this and sharing code too. I tried to get this going but got stuck with the event store setup on docker. I managed to resolve using this thread github.com/EventStore/EventStore/i... and amending the docker command to set dev mode :
docker run -d --name eventstore -p 2113:2113 -p 1113:1113 -e EVENTSTORE_DEV=true eventstore/eventstore
omg no one replied to this? this is fantastic thank you.