Implementing Event Sourcing using .NET
July 18, 2023
by Roman Malyi, Senior Software Engineer
1. Introduction to event sourcing
Event Sourcing is an architectural pattern which postulates that any change in the business state should be stored as a sequence of events. Not only can we query these events, but we can also use the event log to reconstruct past states and calculate the current state of an entity.
In the real world, learning Event Sourcing has multiple benefits. It provides an audit log out of the box, as each event stored is a factual history of the system. This can be invaluable for certain businesses, such as finance, where accurate historical record-keeping is critical. It also makes debugging easier as you can ascertain exactly what led to the current state by replaying the events.
Moreover, Event Sourcing facilitates temporal queries which allows you to view the state of the system at any point in time, an advantage for applications in areas like insurance or legal services. It supports distributed systems as events can be processed asynchronously, adding flexibility for large-scale modern applications. Finally, Event Sourcing can be instrumental in achieving data resilience and can help future-proof systems by enabling easier evolution and adaptation to changing business requirements.
By understanding Event Sourcing, businesses and developers can create systems that are more robust, scalable, and suitable for an evolving digital environment.
Nowadays almost everyone has at least one bank account. Imagine the situation when you do not agree with the amount of money that is specified for one of your cards, you think that one of the transactions was subtracted by mistake. How to prove that you are right? Definitely you need to have the possibility to go through all of the transactions that happened and verify that they are correct. This concept is called audit log. There is one more expectation from such systems - no one should be able to edit an old data, because if an old transaction can change what should we do with all history that depends on it? This is called immutability. Immutability and audit log are two main ideas behind the event sourcing and this is not only about bank accounts. You can do this modeling approach for every business you come across.
Event – an immutable distinct domain object that represents state change.
Event stream – a group of events related to one aggregate. Each event is stored together with a sequence number and stream identifier.
Sequence number – a natural number starting at 1 and representing the event position in the stream.
Stream identifier – in most cases, an aggregate identifier that helps to group related events.
Aggregate – a group of events that represent a single entity within the system.
Snapshot – a specific version of the aggregate stored as a single record in the event store in order to optimize read performance in cases when one aggregate is built from a big number of events, 1000 or more. The ES system can work without snapshots.
Event store – a database that stores the event streams and supports optimistic concurrency control based on sequence number.
Projection – a transient state built using a projector function from a series of events from one or multiple event streams. Typically it’s a denormalized representation of one or more aggregates and is optimized for read performance. Projections are stored in a different database and are used for purposes like generating reports, providing real-time analytics, etc.
Event handler – a piece of code in charge of processing domain events, modifying the application's state, or initiating additional actions based on the data in the event.
Command – a user-initiated request or action from another system that tries to change the state of the application.
Today I want to demonstrate how you can create an application based on Event Sourcing. I will be using Visual Studio 2022, .NET 6 and Event StoreDB.
2. Select the database
EventStoreDB is a database specifically designed for Event Sourcing and related patterns. It excels at storing, retrieving, and querying event data, unlike traditional SQL or NoSQL databases which aren't inherently geared toward event-based storage. EventStoreDB supports multiple event streams, temporal queries, and can project events into read models. It also offers streams' metadata, optimistic concurrency, and support for complex versioning which are paramount for Event Sourcing but not readily available or easy to implement in generic SQL or NoSQL databases. Thus, using EventStoreDB for Event Sourcing can enhance performance, reduce complexity, and improve the robustness of your application.
Note: It's possible to use other databases for ES. I understand that it's not always an easy task to add a new database for your solution or project. You can implement your custom solution based on SQL or NoSql.
3. Setup EventStore database
You can easily run EventStoreDB in a Docker container as a single node, using insecure mode. It's good enough for local development purposes.
Run the command:
docker run --name esdb-node -it -p 2113:2113 -p 1113:1113 eventstore/eventstore:latest --insecure --run-projections=All --enable-atom-pub-over-http
4. Create a project
In order to demonstrate ES I will create a Point of Sale project. The main entity is a basket. You can add or remove products from the basket. It's important to know the value of the entire basket. It can be valuable to see how users add or remove items because we can suggest some of the discarded items in the future and create some analytics that can help us to generate new ideas to improve our services.
Once created, you will see the following structure:
5. Create a basket domain
I won't go into the details of each step and file to save time, you can view the entire project here.
The main model is the Basket:
It contains several fields and logic to calculate the amount and discount.
In order to perform operations with the basket, we need the Aggregate Root object. It is the entry point for any interaction with the aggregate, which is the cluster of objects that are associated with the aggregate root and follow its rules.
The basket aggregate will respond to events and change its state. In order to restore the state of the basket at any point in time, we need to process the events that happened up to that point.
The class BasketAR.cs is inherited from AggregateRoot and implements the logic for each specific event that we need.
For example, create Method:
I check if the basket has not already been created before and if not, I apply the appropriate event.
Also, you can see that I use Result to avoid using exceptions. You can read more here.
6. Connect to event store
Install the .NET client API package to your project (EventStore.Client.Grpc.Streams).
Open Program.cs and add
Now we can create BasketEventStore:
BasketEventStore above uses EventStoreClient class which we get from the Nuget package for interacting with streams. And two main methods: AppendToStream and ReadStream.
Within the event store, the events referring to a particular domain or domain object are stored in a stream. Event streams are the source of truth for the domain object and contain the full history of the changes. You can retrieve state by reading all the stream events and applying them one by one in the order of appearance.
A stream should have a unique identifier representing the specific object. Each event has its own unique position within a stream. This position is usually represented by a numeric, incremental value. This number can be used to define the order of the events while retrieving the state. It can be also used to detect concurrency issues.
Event stores are built to be able to store a huge number of events efficiently. You don’t need to be afraid of creating lots of streams, however, you should watch the number of events in those streams. Streams can be short-lived with lots of events, or long-lived with fewer events. Shorter-lived streams are helpful for maintenance and makes versioning easier.
In order not to work directly with streams, I created another additional wrapper - BasketRepository.
The repository performs a fairly simple task, retrieves or adds events from the event database, and interacts with the aggregate.
7. Use aggregate in the controller
Now we can create and edit our basket using the basket aggregate root and repository.
I created three endpoints to create a basket, add a new item, and get information by id.
After a few calls I can open the EventStore database and see the events that are stored.
You can view the entire source code here.
In wrapping up, this blog post should have helped you understand Event Sourcing and EventStoreDB better. It's a unique way to manage data, keeping track of all changes like a detailed diary.
Now that you know more about it, I hope you're excited to try it out. There's a lot you can do with EventStoreDB, and it might just become your new favorite tool for storing data. Remember,
learning doesn't stop here. As you start using Event Sourcing, you're likely to face some challenges and make some discoveries. I'd love to hear about all that. So, please drop a comment about what you learn and how this blog post helped you. Your experiences will help everyone understand Event Sourcing better. So, let's keep this conversation going. Happy experimenting with EventStoreDB!