C# Event Sourcing framework using Records objects
RecordPlayer is an application framework intended to make it easier and faster for developers to build reliable event-sourced systems with real-time user interfaces. The user interface layer uses Blazor WebAssembly and SignalR to create an experience where the client sends micro-updates to the server and the server sends notification events to the client over a real-time web socket communication channel. The goal is to have a user see updates appear on screen instantly when other users make changes to the system from other computers. The View Model object for each screen presented to the user is a subscriber for specific kinds of notification events that are sent from the server in the form of a Record. In this way, the View Model can "play the Record" to update the state of the client side user experience when new Records are pushed from the server to the client.
On the Web server, RecordPlayer uses SignalR to communicate with the connected Clients in real-time with push notifications. Internally, it uses an Actor System to maintain the tracking state for each client and manage event subscriptions and routing of messages. When the client opens a particular view, it will first query the Web server for the starting state of that view. The RecordPlayer Web will pull that starting view model from the projections storage and send it to the client. Then, it will set up a subscription so that any event Records that target or modify that view model will also be broadcast to that client so they can be played back into the view causing a real-time update of the client with changes that happen from anywhere in the system.
When commands are sent from the Client to the Web server, they are stored in the Log database. The purpose of the Log is two-fold. First, it is a storage system that stores a stream of Record objects. Each Record has a stream identifier attached to it. When a Record is persisted, it is then pushed to the RecordPlayer API as a notification. This ensures that the API will process all Records in a timely manner and the stream identifier for each Record tells the API which Entities should be notified that the new Record is available. There are two different kinds of Records that are important. First, there are Command Records. Commands are instructions from the RecordPlayer Web that travel through the Log storage to the API to inform one or more Entities in the API that a change has been requested. The Entity will process the command and determine what should happen based on the request. Normally this means that the state of the Entity will need to mutate so the entity will emit an Event Record to the Log storage encapsulating the changes. If the command generates some kind of error or fails to process then the Entity will send an update to a projection called the Response View Model that routes the response Record back to the RecordPlayer Web and ultimately though SignalR back to the client so the result of the requested operation can be shown to the user. When an Entity does mutate, the Event Record is also stored in the Log storage with a stream identifier that associates it with the entity that created it. The Log then automatically routes that event back to the RecordPlayer API so that all the event subscribers can be notified. Of course, one of the subscribers will be the Entity itself, but there will also be additional subscribers that manage the projection of view models into the Projection database.
When a new Record is routed to the RecordPlayer API, the API will pass that Record to all the subscribers for that specific kind of record. Normally this means hydrating an Entity into RAM so that it can process the Record. Entities are Event Sourced objects, so the process for hydrating it involves a few steps. First, the RecordPlayer API will look into the Models document store for a persisted version of the entity. If it exists then the entity is restored from the saved state and if not then a new Entity is created with a default state. All Entities have a Record Position property. If the Entity was restored then the position will be the Record Number of the last Record successfully applied to the Entity. If it was a new, default Entity then the position will be zero. Next, the RecordPlayer API will load all the Event Records from the log that are tagged with the stream identifier for this Entity and "play back the records" by applying them to the Entity. This will bring the Entity up to the most current state known then the RecordPlayer API can apply the new Record to the entity causing it to emit new Event Records to the Log, send a Response Record to the Projection, or even send view model updates to the Projection database.