Click here to Skip to main content
15,064,351 members
Articles / Programming Languages / C#
Article
Posted 24 Oct 2017

Tagged as

Stats

44.6K views
1.2K downloads
27 bookmarked

Simple Event Sourcing Demo in C#

Rate me:
Please Sign up or sign in to vote.
5.00/5 (21 votes)
24 Oct 2017CPOL7 min read
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

Image 1

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

Image 2

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:

Image 3

The Tracking Service

Image 4

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

Image 5

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:

Image 6

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Emmanuel Nuyttens
Architect REALDOLMEN
Belgium Belgium
Working in the IT-Branch for more then 20 years now. Starting as a programmer in WinDev, moved to Progress, shifted to .NET since 2003. At the moment i'm employed as a .NET Application Architect at RealDolmen (Belgium). In my spare time, i'm a die hard mountainbiker and together with my sons Jarne and Lars, we're climbing the hills in the "Flemish Ardens" and the wonderfull "Pays des Collines". I also enjoy "a p'tit Jack" (Jack Daniels Whiskey) or a "Duvel" (beer) for "l'après VTT !".

Comments and Discussions

 
GeneralMy vote of 5 Pin
Hyland Computer Systems25-Oct-17 9:23
MemberHyland Computer Systems25-Oct-17 9:23 
Questionsimilar to WF state machine, isn't it? Pin
Fabio Barbieri25-Oct-17 2:35
MemberFabio Barbieri25-Oct-17 2:35 
AnswerRe: similar to WF state machine, isn't it? Pin
Emmanuel Nuyttens25-Oct-17 5:05
MemberEmmanuel Nuyttens25-Oct-17 5:05 
Hi Fabio,

Yes indeed, you can see it that way Smile | :) . To answer your question to give you an estimation on the effort needed to build a full application of it is a bit difficult, as the word "full" is a bit vaque to me. But sure, you can add a persistent storage to it (sql-server) and enhance the model classes to meet your needs, but the sample-app was just made to scratch the surface on the subject.

Kr,
Emmanuel.
GeneralYour sample seems to be too simple for me Pin
Klaus Luedenscheidt24-Oct-17 18:47
MemberKlaus Luedenscheidt24-Oct-17 18:47 
GeneralRe: Your sample seems to be too simple for me Pin
Emmanuel Nuyttens24-Oct-17 21:17
MemberEmmanuel Nuyttens24-Oct-17 21:17 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.