Real implementations: KurrentDB/EventStoreDB + Orleans
So far we built the ideas with minimal code. Now letβs map them to real .NET tooling.
Option A: Event-native database (KurrentDB / EventStoreDB)β
KurrentDB (formerly EventStoreDB) is a common choice for event sourcing because it directly supports:
- streams
- optimistic concurrency
- subscriptions (push-based projections)
Append with optimistic concurrencyβ
Concept: read the last revision, then append with expected revision.
using System.Text.Json;
using KurrentDB.Client;
var client = new KurrentDBClient(
KurrentDBClientSettings.Create("kurrentdb://admin:changeit@localhost:2113?tls=false")
);
var streamName = "bank-account-11111111-1111-1111-1111-111111111111";
var opened = new { AccountId = "11111111-1111-1111-1111-111111111111", OwnerName = "Jeevan" };
var eventData = new EventData(
Uuid.NewUuid(),
"AccountOpened",
JsonSerializer.SerializeToUtf8Bytes(opened)
);
// Expect stream does not exist yet:
await client.AppendToStreamAsync(streamName, StreamState.NoStream, new[] { eventData });
// Later: optimistic concurrency based on last event number
var lastEvent = client
.ReadStreamAsync(Direction.Forwards, streamName, StreamPosition.Start)
.LastAsync();
var deposit = new EventData(
Uuid.NewUuid(),
"MoneyDeposited",
"{\"amount\": 50}"u8.ToArray()
);
await client.AppendToStreamAsync(
streamName,
lastEvent.OriginalEventNumber.ToUInt64(),
new[] { deposit }
);
Subscriptions (for projections)β
Typical approach:
- subscribe to a stream (or to all streams)
- apply events to a projection
- checkpoint your position
await using var sub = client.SubscribeToStream(streamName, FromStream.Start);
await foreach (var message in sub.Messages.WithCancellation(CancellationToken.None))
{
if (message is StreamMessage.Event(var evnt))
{
var type = evnt.Event.EventType;
var json = evnt.Event.Data.Span;
// Apply to projector here.
// type + json => domain/integration event => read model update
}
}
Option B: Orleans event-sourced grainsβ
Orleans supports event-sourced grains via JournaledGrain.
This is most compelling when:
- youβre already using Orleans actors
- you want event sourcing βinsideβ actor semantics
Orleans model:
- grain state is derived from events
- you implement
Applymethods orTransitionState - providers decide where/how events/state are stored
Orleans example from docs:
using Orleans.EventSourcing;
using Orleans.Providers;
[StorageProvider(ProviderName = "OrleansLocalStorage")]
[LogConsistencyProvider(ProviderName = "LogStorage")]
public class EventSourcedBankAccountGrain : JournaledGrain<BankAccountState>
{
// ...
}
If you go Orleans:
- learn their consistency providers
- understand confirmation/replication behavior
- treat event evolution the same way (events are contracts)
Choosing between them (pragmatic)β
- If you want a dedicated event store with strong stream semantics: KurrentDB/EventStoreDB
- If your domain already fits the actor model and you want ES built-in: Orleans
- If you want event sourcing but inside a relational/document DB: use a battle-tested library (e.g. Marten) rather than inventing storage
Nextβ
Last step: the βdonβt step on rakesβ checklist.
Sourcesβ
https://github.com/kurrent-io/kurrentdb-client-dotnet/blob/master/docs/api/appending-events.mdhttps://learn.microsoft.com/en-us/dotnet/orleans/grains/event-sourcing/