Skip to content

Getting started

Tomas Lycken edited this page Oct 2, 2017 · 19 revisions

The RdbmsEventStore has a few common components, and a few components specific to the backing database engine.

The Event Registry

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 strings (event names) and System.Types (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 Event Serializer

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();

The Event Factory

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.

The Write Lock

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...

Event Types

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.

Choosing a backing data store

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

Continued: using the event store

You should now have been able to create an event store instance, eventStore.

Writing events to the event store

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).

Reading events from the 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));

Updating state of aggregates

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.

Where to go from here

Now that you've seen the basics, you probably want to check out these pages:

Hooking up your IoC container