-
Notifications
You must be signed in to change notification settings - Fork 2
Getting started
The RdbmsEventStore has a few common components, and a few components specific to the backing database engine.
1. Events
2. The Event Factory
3. Storage Engines
In order to keep track of your event types, and to handle serialization and deserialization of event payloads, you must build an event registry for your events. In its simplest form, it is just a bi-directional dictionary between string
s (event names) and System.Type
s (event types). In most common use cases, you can use an AssemblyEventRegistry
, that does a lot of the work for you:
var eventRegistry = new AssemblyEventRegistry(
typeof(SomeEventType),
namer: type => type.Name,
inclusionPredicate: type => type.Name.EndsWith("Event"));
The namer and predicate are optional, defaulting to type => type.Name
and type => true
.
If you don't want any extras, the default IEvent<TStream>
has the following shape:
public IEvent<out TStreamId>
{
TStreamId StreamId { get; }
DateTimeOffset Timestamp { get; }
long Version { get; }
Type Type { get; }
object Payload { get; }
}
The properties Type
and Object
refer to
In order to ensure that you don't have multiple threads adding events simultaneously, your event store should depend on the write lock. There is an interface IWriteLock
and a default implementation WriteLock
that just wraps an AsyncLock
from Stephen Cleary's AsyncEx suite:
var writeLock = new WriteLock();
It is very important that the write-lock instance is singleton across the application. Otherwise, it won't do much good...
This is where it gets interesting, but also where we have to split up based on the backing data storage you'll be using.
You should now have been able to create an event store instance, eventStore
.
In order to write events to the store, you take a dependency on the IEventWriter
interface, implemented by the event store, and use its Commit
method:
var event = SomeActionResultingInAnEvent(); // event is an instance of one of your POCO events
await eventStore.Commit(streamId, versionBeforeThisEvent, event);
It works with lists too, so if you have an action that results in multiple events, you can commit them in one go. The streamId
is a way to shard the event list based on e.g. domain aggregates or features. The versionBeforeThisEvent
is a long
value included in the metadata of the last event you read from the store before you took the action, and is used for conflict detection (it must equal the highest version currently in the event store, for that stream).
You can fetch events using the IEventStream
interface:
var events = eventStore.Events(streamId);
However, often you won't want all events from a stream for performance reasons. There is therefore also an overload that takes a projection from IQueryable<TEvent>
to IQueryable<TEvent>
, which lets you filter further:
var events = eventStore.Events(streamId, es => es.Where(e => e.Timestamp > lastKnownEvent));
Since the process of updating your application state based on the wrapper TEvent
will, for each type of event, involve the same tedious process of unpacking and deserializing the payload, there is a Materializer
class that can help you with that:
var materializer = new Materializer(eventRegistry, serializer);
var events = eventStore.Events(streamId);
var state = materializer.Unfold(initialState, events, (s, e) => s.Evolve(e));
Here, initialState
is an instance of the same type as state
and the lambda at the end is a calculation of the next state given the current state and an event. For example, if the state is the position on a chess board, and the event is a move, then the lambda should return the position on the board after the move.
Now that you've seen the basics, you probably want to check out these pages: