-
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.
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),
type => type.Name.EndsWith("Event"));
The predicate is optional, and only types for which it returns true
are added to the registry.
The serializer is responsible for serializing and deserializing your event payloads. The default implementation just serializes to JSON, which is probably fine for most use cases:
var serializer = new DefaultEventSerializer();
RdbmsEventStore is extensible, so that you can create your own event type and add whatever metadata you need. Therefore, the implementation depends on having an event factory available, which will take your event payloads, serialize them, add metadata and return an instance of the event type you'll persist. In most cases, though, you'll be fine with an event type that's just a plain implementation of the IEvent<TId>
interface (the generic type parameter is the type of the event and stream ids), and for that case the default event factory will be enough:
var eventFactory = new DefaultEventFactory(eventRegistry, serializer);
Since the event factory is responsible for the serialization, this is the class you override if you want to use a different serialization scheme.
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...
There are two "levels" of event types in RdbmsEventStore: one of them are your own POCO events, that require no ceremony or special interfaces other than the possibility to serialize and deserialize them. The other is the wrapping implementation of IEvent
, which carries metadata such as EventId
and StreamId
as well as the serialized payload from your POCO event.
This is where it gets interesting, but also where we have to split up based on the backing data storage you'll be using.
Currently, only EF6 is implemented, but there are plans for at least EF Core and SQL Server without EF as well. From here, continue the guide that's appropriate for your backing store, and then come back here for the final notes.
Getting Started: Using Entity Framework 6
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).
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: