Skip to main content

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 Apply methods or TransitionState
  • 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.

Next: Pitfalls checklist

Sources​

  • https://github.com/kurrent-io/kurrentdb-client-dotnet/blob/master/docs/api/appending-events.md
  • https://learn.microsoft.com/en-us/dotnet/orleans/grains/event-sourcing/