Getting Started with EventStoreDb (C#)

Callum Linington
8 min readMar 15, 2023

--

If you’re starting out with Event Sourcing, or you’re wanting to try out another way of storing information then EventStore may be the database for you!

If you want to know how to get started with using EventStoreDb in your C# ASP.NET Core project, then look no further.

Event Sourcing

The idea of Event Sourcing is treating all the changes that we make to our Domain Object (Aggregate) are tracked by creating a Domain Event that describes the execution outcome which is stored in an Event Store. When we want to then get the latest Domain Object (Aggregate), it is done by querying all those Domain Events and applying each change reconstituting the final object.

Fortunately, if most developers have used Git, then most developers have a good understanding of Event Sourcing — maybe without knowing it. Much like with Git, any changes are tracked and committed to the repository. When we checkout a branch, all the history gets replayed in order and our final file system representation is built up.

Event Sourcing Picture

This is referenced from Implementing Domain Driven Design — Vaugh Vernon pg 161.

EventStoreDb allows us to remove some of the parts of the original diagram. You could implement an outbox pattern to do guaranteed delivery to an Event Store — but with EventStoreDb, we can just write directly to it and skip that part without losing any of the benefit.

Why would you choose Event Sourcing over relational/document/graph databases? You wouldn’t necessarily choose it over those, it would be chosen alongside. The pattern some people do is to use these database types as a cache style DB, that also gives you the power of that DB’s querying (you can’t query an Event Store, other than a linear read event by event). The benefit of this, is that your Event Store becomes the source of truth, and you can re-build the cache DBs at any time to the correct state — remember that your cache DB doesn’t necessarily need to be the intended representation of the events. You have the choice about how you build the DB based on all the streams that exist in the Event Store.

Streams

What is a stream? We’ve talked about events, we’ve talked about an Event Store — but we’ve only just touched on a stream. With a traditional database, it needs to house all these rows of data, or sets of documents into something… tables or collections. We can think of a stream as a collection of events. A stream nicely describes the mental model for how these work

Stream

In a traditional database we may actually make changes to a particular row or document. This is a no go, in Event Sourcing it’s imperative that each event is an immutable piece of data — but what if the previous event was a mistake, or needs to be changed? That’s a new event.

Immutable Event Stream

With the picture above, the illustration is of my bank account. At the start of the day I have £100 (I wish…). Some time passes and I decide to go buy birthday present for my brother for £20. The next balance query will show that I have £80. Oh no, this was a mistake, the present was the wrong one. I can’t undo the past, but what I can do is get a refund to return me back to the position I was before I bought the present.

I don’t need to go rectify the payment that I made, that could cause a whole host of problems, and some serious technical challenges. It’s actually far easier and more expressive to log a new event that describes a refund. Also, this is a beautiful representation of our DDD concepts in our code.

Getting Started

dotnet new webapi -n MyEventStore

cd MyEventStore && rider MyEventStore.csproj

Open the .csproj file and add

<PackageReference Include="EventStore.Client.Grpc.Streams" Version="22.0.0" />

To the <ItemGroup> section.

Spin up EventStoreDb

Back to your shell

docker run --name myeventstore -d --restart unless-stopped -p 2113:2113 -p 1113:1113 eventstore/eventstore:latest \
--insecure --run-projections=All \
--enable-external-tcp --enable-atom-pub-over-http

This will spin up an instance of EventStoreDb in your docker. I like to add --restart unless-stopped because I want it to automatically spin up with the docker service starting.

Configure ASP.NET Core

Open Program.cs

Option 1:

builder.Services.AddEventStoreClient( new Uri( "esdb://localhost:2113?tls=false" ) );

Option 2:

open appsettings.Development.json

{
"EventStore": "esdb://localhost:2113?tls=false"
}

back to Program.cs

builder.Services
.AddEventStoreClient( builder.Configuration
.GetSection( "EventStore" )
.Get<string>( ) );

Pick the option that suits you best. I like the 2nd option, so that when I start deploying in other environments there is no code change.

Configure App

Firstly, make a quick change to WeatherForecast.cs :

public DateOnly? Date { get; set; }

Adding that ? to the Date property.

Next, we need to create the event that will be stored in our EventStoreDb.

Create a new file (or stick this class in the WeatherForecastController.cs

public class WeatherForecastRecorded
{
public static string StreamName = "WeatherForecast";

public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public string Summary { get; set; }
}

Here we’re just defining some arbitrary fields that we would like to store. The point in time state. I like to have a static, or const field that has the Stream Name in (no magic strings).

Event name should be in the past tense. SomethingCreated , SomethingUpdated , TransactionCompleted , JobStarted .

Head over to the WeatherForecastController.cs

Update the constructor:

private readonly EventStoreClient client;

public WeatherForecastController( ILogger<WeatherForecastController> logger,
EventStoreClient client )
{
this.logger = logger;
this.client = client;
}

Add a POST method:

[HttpPost]
public async Task<object> PostAsync( WeatherForecast data )
{
}

All code from here, until I say otherwise will be in this post method.

Firstly we can construct the event:

var weatherForecastRecordedEvent = new WeatherForecastRecorded
{
Date = DateTime.Now,
Summary = data.Summary,
TemperatureC = data.TemperatureC
};

Simply mapping from the request body WeatherForecast to the WeatherForecastRecorded . How you want to do this is up to you. I would suggest that when you instantiate an event might, this might not be near the API interface. So, it may seem unnecessary right now, but will make sense when the app expands.

Next we need to make an EventData class:

var utf8Bytes = JsonSerializer.SerializeToUtf8Bytes( weatherForecastRecordedEvent );

var eventData = new EventData( Uuid.NewUuid( ),
nameof( WeatherForecastRecorded ),
utf8Bytes.AsMemory( ) );

So, several things are going on here. Firstly, we have to turn our event into some ReadOnlyMemory<byte> to store in the EventData . Secondly, when we construct the EventData we need to supply 3 things. Uuid which should be obvious, but is the unique identifier for this event. We need the EventType which we can just extrapolate from the name of the event class we created. Finally we need to pass our JSON byte data. N.B. it’s important to note, that if you omit the contentType parameter, then it defaults to application/json . Which is ideal for our scenario, but something to be aware of.

Finally to finish off our Post method we need to add:

var writeResult = await this.client
.AppendToStreamAsync( WeatherForecastRecorded.StreamName,
StreamState.Any,
new[ ] { eventData } );

return writeResult;

this will append your EventData to the stream. Voila. Be careful when setting either StreamState or StreamPosition fields. You can use these to narrow down the scenario in which it’s valid to append the data. For example, if you use StreamPosition , then you can implement optimistic concurrency (another blog post I feel).

The way I have configured that append method, is that this should always succeed***** (there’s always a chance of failure… :D ).

Hit Your API

Now you can hit run, or dotnet run . Once you do this, navigate to http://localhost:{PORT}/swagger , this should open up your swagger documentation. Head to the post part, and click Try it Out and paste this body:

{
"temperatureC": 50,
"summary": "Very Hot Day"
}

When you’re happy with the body, go ahead and execute the request.

Or, if you’re a cURL user:

curl -X 'POST' \
'http://localhost:5096/WeatherForecast' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"temperatureC": 50,
"summary": "Very Hot Day"
}'

Open a new tab in your browser, and go to http://localhost:2113 , it should open up something like this:

EventStore dashboard

Navigate to the Stream Browser

Stream Browser

You should see WeatherForecast or whatever you called your stream somewhere. When you click on it, you should be presented with this:

Stream Viewer

Then you can click on the Name of your event, 0@WeatherForecast for me.

Event Recorded

We can see a few things here; we can see the stream, the Type which is the class we used. Most importantly, we can see the JSON data that we serialised. Amazing!

What Next?

There is a client.ReadStreamAsync which when provided a stream name will retrieve the results:

var streamResult = this.client.ReadStreamAsync( Direction.Forwards,
WeatherForecastRecorded.StreamName,
StreamPosition.Start );
await foreach ( var item in streamResult )
{
item.Event.EventType // <-- use this to determine which class to serialise to
JsonSerializer.Deserialize( item.Event.Data.Span,
typeof( WeatherForecastRecorded ) );
}

That’s your basic impl. You can map the EventType string to a Type by doing something like typeof(AnyTypeInYourAssembly).Assembly.GetTypes().First(t => t.Name.Equals(EventType, StringComparison.OrdinalIgnoreCase)) .

Feel free to put any amount of different types on to a single stream. There’s no limitation 1:1 relationships onlyEventType <=> Stream .

Once you’ve got the type, then either you can rebuild a single object based on it, or just have a list of data that can be queried.

Conclusion

Hopefully you’ve got a basic idea of how to get started with EventStoreDb, and I hope this spurs you on to try it out, play with all its features and discover it as a new data store.

It should be fairly simple, I find this way easier than Efcore or any other involved database.

I will write more blog posts to go through other concepts at a later date.

There’s things to cover like, Stream Subscriptions and also using Discriminated Unions to represent a Stream.

Anyways, let me know if this helped you, and what your experiences are in the comments!

--

--