Simple Event Sourcing Demo in C#





5.00/5 (22 votes)
Example on how to use Event Sourcing in C#
Introduction
While looking at the concepts of DDD (Domain Driven Design) I came across the event sourcing principle. After hassling through some theory on the subject (next site owned by Martin Fowler give some good insights on the subject: https://martinfowler.com/eaaDev/EventSourcing.html) I came to the idea to put my knowledge in practice by providing a simple C# based sample which briefly illustrates this pattern.
Background
The general idea of event sourcing is to ensure that every change to the state of an application is captured in an event object, and these event objects are themselves stored in the sequence they were applied. Simply said event sourcing contains a log of changes applied to an object. Recording these subsequent changes can become handy when you want to replay the changes made to your objects (for whatever reason ...)
The sample application
The sample application is kept quite simple. It is based on the Ship Tracking Service sample provided by Martin Fowler on his site (check Introduction for details). The remainder of this article explains the implementation of the sample code. Please note, as I only want to explain the pattern behind event sourcing I did not introduce any professional messaging infrastructures (e.g. NServiceBus) and associaties queueing mechanisms, as we should normally rely on in a production system.
The Use Case
The code case is quit simple. We have Ports (Harbors) and we have Ships. Next a Ship arrives (Arrivals) or leaves (Departures) a certain Harbor. When a ship leaves a Port, then it is AT SEA else it is in a specific Port or none of these in case the Ship is in maintenance mode (maintenance mode is out-of-scope here ...). Finally we have a Ship Tracking Service which Tracks Ship arrival or departure. The project has some "dummy" Ships and some "dummy" Ports defined and Port arrivals are taken at random (thus possible is the case that a Ship leaves and arrives multiple times in same Port, maybe when the crew forgot their boterhammekes on the Port ;-) ). (Boterhammekes is dutch translation for slices of bread).
The Domain Model
So our related domain model is quit simple. We have a userinterface which consists of a single form FormShipTrackingService, this form starts an instance of the ShipTrackingService which includes a timer that get's called every 2 seconds. We simulate an arrival or departure of a ship on each timer tick interval. The arrival/departure is called a ship tracking event. So on each ship tracking event, we record this event in the UI : arrival time, recording time, ship and port are recorded. Multiple arrivals/departures of a ship lead to multiple change-records in the processing log. Finally the tracking processor notifies the ship to update itself on each tracking event (port update). That's all what's happening in this simple use case. After knowing this, let's look at some of the implementation details.
The Models
The Ship Model
using TrackingService.DomainEvents; namespace TrackingService.Models { public class Ship { #region Properties public int ShipId { get; set; } public string Name { get; set; } public Port Location { get; set; } #endregion Properties #region Public Interface public void HandleArrival(ArrivalEvent ev) { // Here we set the Port to the Port Set by the ArrivalEvent Location = ev.Port; } public void HandleDeparture(DepartureEvent ev) { // Here we set the Port to the Port Set by the DepartureEvent Location = ev.Port; } #endregion Public Interface } }
Ship models has a unique id, name and location (port). It also contains two methods that will be called by the tracking processor to update the ship's state after been tracked.
The Port Model
namespace TrackingService.Models { public class Port { #region Properties public int PortId { get; set; } public string Name { get; set; } #endregion Properties #region Public Interface public override string ToString() { return Name; } #endregion Public Interface } }
Each Port has a name and is defined by a unique id.
The Domain Event Classes
Next our sample contains some domain event classes.
DomainEvent
namespace TrackingService.DomainEvents { // Domain event is the base event class // it simply registers when the according // event occured and is recorded public abstract class DomainEvent { #region Private Storage private DateTime _recorded, _occured; #endregion Private Storage #region Internal Interface internal DomainEvent(DateTime occured) { this._occured = occured; this._recorded = DateTime.Now; } abstract internal void Process(); #endregion Internal Interface } }
On Top of the hierarchy we have the DomainEvent
class This class defines common properties like occurence and recording times. It also defines the abstract Process
method which has to be implemented by depending classes.
ShippingEvent
using TrackingService.Models; using TrackingService.Services; namespace TrackingService.DomainEvents { // The ShippingEvent enherits from the base DomainEvent // And adds logic to keep track of the Ship, Port and Trackingtype (Arrival,Departure) public abstract class ShippingEvent : DomainEvent { #region Private Storage private Port _port; private Ship _ship; private TrackingType _trackingType; #endregion Private Storage #region Public Properties public Port Port { get { return _port; } set { _port = value; } } public Ship Ship { get { return _ship; } set { _ship = value; } } public TrackingType TrackingType { get { return _trackingType; } set { _trackingType = value; } } #endregion Public Properties #region Internal Interface internal ShippingEvent(DateTime occured, Port port, Ship ship,TrackingType trackingType) : base(occured) { this._port = port; this._ship = ship; this._trackingType = trackingType; } #endregion Internal Interface #region Public Interface public override string ToString() { return $"TrackingType: {this.TrackingType} Ship: {this.Ship.Name} Port: {this.Port.Name}"; } #endregion Public Interface } }
ShippingEvent
is the base Shipping class. It adds shipping specific properties and behavior to the parent domain event class. The shipping event class keeps track of the Ship, Port and Tracking Type (Arrival,Departure,None) and it uses it's base class to set the inherited member values.
Arrival and Departure Events
using TrackingService.Models; using TrackingService.Services; namespace TrackingService.DomainEvents { // The arrival Event simply captures the data and has a process method that simply // forwards the event to an appropriate domain object (ship in this case) public class ArrivalEvent : ShippingEvent { #region Internal Interface internal ArrivalEvent(DateTime arrivalTime, Port port, Ship ship, TrackingType trackingType) : base(arrivalTime, port, ship,trackingType) { } internal override void Process() { Ship.HandleArrival(this); } #endregion Internal Interface } } using TrackingService.Models; using TrackingService.Services; namespace TrackingService.DomainEvents { // The departure Event simply captures the data and has a process method that simply // forwards the event to an appropriate domain object (ship in this case) public class DepartureEvent : ShippingEvent { #region Internal Interface internal DepartureEvent(DateTime departureTime, Port port, Ship ship,TrackingType trackingType) : base(departureTime,port,ship,trackingType) { } internal override void Process() { Ship.HandleDeparture(this); } #endregion Internal Interface } }
The ArrivalEvent
and DepartureEvent
classes are quit similar. The first is used to notify the Ship
class from arrival at a Port
, the latter for departure from a Port
.
EventProcessor
namespace TrackingService.DomainEvents { // The Event Processor Processes the Events // as received from the TrackingService public class EventProcessor<T> where T : ShippingEvent { #region Private Storage private IList<T> _eventLogger = new List<T>(); #endregion Private Storage #region Public Interface public void ProcessEvent(T e) { e.Process(); _eventLogger.Add(e); } public int CountEventLogEntries() { return _eventLogger.Count; } public List<T> GetEvents() { return _eventLogger as List<T>; } #endregion Public Interface } }
The EventProcessor
class takes a generic type as parameter, in our case it has been restricted to items of type ShippingEvent
(because shipping event is the base class for logging). So this class get's events from the ShipTrackingService
and processes them. Important to note is that this processing class is the class who add's event sourcing (this by adding the received Arrival or Departure events to the event sourcing log).
Common Application Flow
Application Entry Point
private void FormShipTrackingService_Load(object sender, EventArgs e) { try { // create the tracking service _trackingService = new Services.ShipTrackingService(); // create the eventprocessor _eventProcessor = new EventProcessor<ShippingEvent>(); // subscribe to the ship tracked event of the tracking service _trackingService.ShipTracked += _trackingService_ShipTracked; this.SetDataSource(); SetTimer(); } catch(Exception ex) { MessageBox.Show(ex.Message); } }
The application flow starts in the Load
event of the main form. First we create an instance of the TrackingService
and EventProcessor
. Next we set the DataSources
and start a tracking Timer
. With the tracking Timer wi simulate arrivals/departures of ships. The user interface also subscribes to the ShipTracked
event of the TrackingService. The mentioned items are more detailed below.
Set the DataSource
private void SetDataSource() { _shipsBindingSource.DataSource = null; _shipsBindingSource.DataSource = _trackingService.Ships; }
As we track a list of Ships
I used a BindingSource
as a datasource for the ships grid. The BindingSource
is simply bound to the static list of ships as defined by the tracking service.
Initialize the Tracking Simulation Timer
// we simulate ship arrival/departure every 2 seconds private void SetTimer() { _timer = new System.Windows.Forms.Timer(); _timer.Interval = 2000; _timer.Tick += _timer_Tick; _timer.Enabled = true; }
We use a simple timer object to simulate ship arrival/departures. The timer has an interval of 2000ms (2sec).
Ship Arrival/Departure Tracking
private void _timer_Tick(object sender, EventArgs e) { if(_trackingService != null) { // we simulate tracking events by selecting a RANDOM ship // next tracking type is set to Arrival or Departure depending on the // current location of the selected ship // if port of selected ship == "AT SEA" then we set the tracking event type // as an ARRIVAL and will set ship port to the next port (!= 0) in the Port list // if port of selected ship != "AT SEA" then we set the tracking event type // as a DEPARTURE and set port to "AT SEA" int maxShip = _trackingService.Ships.Count; // select a random ship in the list _selectedShipId = _randomShip.Next(1, maxShip); // set tracked ship to the current selected id _trackingService.TrackedShip = _trackingService.Ships[_selectedShipId]; // set the tracking event type (Arrival or Departure) depening on the current location (PortId 0 is AT-SEA) _trackingService.TrackingType = _trackingService.TrackedShip.Location.PortId == 0 ? TrackingType.Arrival : TrackingType.Departure; // set the time of tracking recording _trackingService.Recorded = DateTime.Now; // create a unique id for the tracking _trackingService.TrackingServiceId = Guid.NewGuid(); // set the new port of the tracking, this is a random port in // the list in case of arrival // or port 0 = AT SEA in case of departure if(_trackingService.TrackingType == TrackingType.Arrival) { int maxPort = _trackingService.Ports.Count; _selectedPortId = _randomPort.Next(1, maxPort); } else { _selectedPortId = 0; } _trackingService.SetPort = _trackingService.Ports[_selectedPortId]; // handle the tracking by the tracking service // the tracking service will now send an Arrival event or Departure event to the Ship _trackingService.RecordTracking(_eventProcessor); // augment number of events _numberOfEventsCount.Text = _eventProcessor.CountEventLogEntries().ToString(); // refresh the UI this.SetDataSource(); } }
As already mentioned before, with the timer_tick
event we simulate the ship tracking. The coding comments should be clear enough to understand the behavior of this event.
Show the EventSource Log
private void toolStripButtonShowEvents_Click(object sender, EventArgs e) { this._eventsTextBox.Text = string.Empty; // show the events for the selected ship try { // first get the selected ship Ship currentShip = (Ship)this._shipsBindingSource.Current; if(currentShip != null) { // get the event logs List<ShippingEvent> events = _eventProcessor.GetEvents() as List<ShippingEvent>; // filter events for the current ship var filterByShip = events.Where(ev => ev.Ship.ShipId == currentShip.ShipId); foreach (ShippingEvent ev in filterByShip) { this._eventsTextBox.Text += ev.ToString() + "\r\n"; } } } catch(Exception ex) { MessageBox.Show(ex.Message); } }
Every ship event (arrival/departure) is logged by the EventProcessor
. This is the core of the Event Sourcing mechanism. The user can show a list of tracking events (which are changes in state of the selected ship), as shown next:
The Tracking Service
There is one piece of code that we didn't detail yet, and this is the behavior of the ShipTrackingService
itself. The tracking service and it's dependencies are stored in the Services folder. I will go briefly through each in the next sub sections.
Tracking Type Enum
namespace TrackingService.Services { public enum TrackingType { Arrival, Departure, None}; }
Enums which holds the different possible tracking types.
ShipTracked EventArgs
using TrackingService.Models; namespace TrackingService.Services { // Holds the properties of the Ship TrackingService // that are exposed through event-handeling public class ShipTrackedEventArgs : EventArgs { #region Public Properties public Guid TrackingServiceId { get; set; } public DateTime Recorded { get; set; } public TrackingType TrackingType { get; set; } public Ship TrackedShip { get; set; } public Port OldLocation { get; set; } public Port NewLocation { get; set; } #endregion Public Properties } }
The ShipTrackedEventArgs
class will be used by the ShipTrackingService
to notify it's subscribers (the ShipTracking-UI in our case ...) that a ship tracking Record
took place. The UI will then use this data to refresh it's content.
Ship Tracking Service
As the number of coding lines for the ShipTrackingService
is to big, I will slice the codebase into different pieces.
ShipTrackingService Slice-1: Event Declaration
#region Event Declaration public delegate void ShipTrackedEventHandler(object sender, ShipTrackedEventArgs e); public event ShipTrackedEventHandler ShipTracked; #endregion Event Declaration
We define a delegate which will provide the necessary interface that a subscriber (UI) can use to update it's content depending on the occured event in the ship tracking service
ShipTrackingService Slice-2: Instance Variables Declaration
#region Private Storage private TrackingType _trackingType = TrackingType.None; private Guid _trackingServiceId; private DateTime _recorded; private List<Port> _ports; private List<Ship> _ships; private Ship _trackedShip; private Port _currentPort; private Port _setPort; #endregion Private Storage
The ShipTrackingService has some state:
Variable | Info |
---|---|
TrackingType | Type of tracking (Arrival, Departure, None). |
TrackingServiceId | Unique identifier for the TrackingService. |
Recorded | Date/Time of tracking recording. |
Ports | Dummy Ports. |
Ships | Dummy Ships. |
TrackedShip | Reference to the Current Tracked Ship. |
CurrentPort | Reference to the Current Tracked Port. |
SetPort | Reference to the New Port destination in case of Arrival. |
Of course each backing variable has it's public property.
ShipTrackingService Slice-3: Initialization
#region C'tor public ShipTrackingService() { // initialize ports _ports = new List<Port>() { new Port() { PortId = 0, Name = "AT Sea" }, new Port() { PortId = 1, Name = "Port of Shangai" }, new Port() { PortId = 2, Name = "Port of Antwerp" }, new Port() { PortId = 3, Name = "Port of Singapore" }, new Port() { PortId = 4, Name = "Port of Dover" } }; // initialize ships _ships = new List<Ship>() { new Ship() { ShipId = 1, Name = "Ship_1", Location = _ports[0] }, new Ship() { ShipId = 2, Name = "Ship_2", Location = _ports[0] } ,new Ship() { ShipId = 3, Name = "Ship_3", Location = _ports[0] }, new Ship() { ShipId = 4, Name = "Ship_4", Location = _ports[0] } }; } #endregion C'tor
In the constructor code we initialize our Ships
and Ports
. Please not that the Location
(Port) property refers to "AT-SEA" port which means that while starting up our ShipTrackingService
every ship is supposed to be AT-SEA (and not in a port ...).
ShipTrackingService Slice-4: Ship Tracking
#region Public Interface public void RecordTracking(EventProcessor<ShippingEvent> eProc) { // Create event depending on TrackingType Port OldLocation = TrackedShip.Location; ShippingEvent ev; if (TrackingType == TrackingType.Arrival) { ev = new ArrivalEvent(DateTime.Now, SetPort, TrackedShip,TrackingType); } else { ev = new DepartureEvent(DateTime.Now, SetPort, TrackedShip, TrackingType); } // send the event to the event handler (ship) which will update it's status on the provided event data eProc.ProcessEvent(ev); // notify the UI Tracking List so it can update itself ShipTrackedEventArgs args = new ShipTrackedEventArgs() { TrackingServiceId = TrackingServiceId, Recorded = Recorded, TrackingType = TrackingType, TrackedShip = TrackedShip, OldLocation = OldLocation, NewLocation = SetPort, }; // notify subscribers ... OnShipTracked(args); } #endregion Public Interface #region Protected Interface // Notify the (UI) Subscribders that a Ship has been tracked protected virtual void OnShipTracked(ShipTrackedEventArgs args) { if (ShipTracked != null) ShipTracked(this, args); } #endregion Protected Interface
The RecordTracking
method is the core of our Tracking Service. This method is called from the UI _timer_Tick
method and it does 2 things: first it creates and delegates a new Arrival or Departure event to the EvenProcessing engine. The EventProcessing engine wil instruct the concerned ship to update it's state depending on the supplied event data. Next it informs the attached subscribers (Tracking Service UI in our case) that a tracking event took place. Next the UI can take appropriate action to update it's state. The event handling code in the UI is shown below:
private void FormShipTrackingService_Load(object sender, EventArgs e) { try { ... // subscribe to the ship tracked event of the tracking service _trackingService.ShipTracked += _trackingService_ShipTracked; ... } ... } // Update the UI after RecordTracked has been tracked in TrackingService private void _trackingService_ShipTracked(object sender, ShipTrackedEventArgs e) { this.TrackingOccuredTextBox.Text += $"TrackingId: {e.TrackingServiceId}\r\n" + $"RecordedAt: {e.Recorded.ToLongTimeString()}\r\n" + $"TrackingType: {e.TrackingType}\r\n" + $"Ship: {e.TrackedShip.Name} Id: {e.TrackedShip.ShipId}\r\n" + $"Current Location : {e.OldLocation.Name}\r\n" + $"New Location : {e.NewLocation.Name}" + "\r\n\r\n"; }
Putting it all together
So now that we have a good understanding of the different parts that make up our application, let's have a look at the user interface. On top we have our list of ships. In bottom of the screen we have the tracking table which shows all the arrivals/departures in sequence of occurence. Next to demonstrate the events sourcing in the middle of the screen we have the log details of a single ship. Ship_3 in our case.
Some Optimalization Points
I tried to make the sample as simple as possible, so i used concrete classes for the implementation. In real environment, you should ommit to directly use concrete classes, because, in many cases we could have different forms of TrackingServices all sharing some common logic and adding specific behavoir. For this reasen the ShipTrackingService should be implemented by using an Interface and use a Dependency Injection mechanism to inject concrete instances of our service in our client class, as shown below: