MessageHandler

In Memory Projection

M

Introduction

The software now records the decissions it takes as events on an event stream.

In this lesson, you'll implement a Projection pattern, which is the pattern assigned to summarize decissions into human readable state.

By the end of this lesson, you will:

  • Understand what the responsibility of a Projection is.
  • Be able to identify the pattern from an Event Model.
  • Have built, and tested, an implementation of the pattern using MessageHandler.

What is a Projection

A projection turns a stream of events into state.

Decissions taken by the system are recorded as events on an event stream.

However, humans may have a hard time understanding the state of the system by looking at this stream of events.

Projection

It is the responsibility of a projection to summarize the decissions taken in the past, into a human readable format of the state at a given point in time.

Identifying Projections

To identify a projection in an Event Model, you search for the transitions between events (orange post-it) and state (green post-it).

Order booking
High resolution version

When BookingStarted occured, and the buyer looks at their booking, then the booking should be shown with a status Pending.

However, when the seller would have confirmed the booking in the meantime, the buyer would expect to see the booking in status Confirmed.

Even though it is not part of the official event model specification, you can indicate that the full history needs to be replayed to obtain the state, using a big dot instead of using a direct arrow.

Implementing the Projection

You're going to implement the projection in a similar way as you did the Aggregate Root, TDD style!

You can pick up the codebase from where you left off last time, it's in this commit

Writing a test

Right click on the OrderBooking.UnitTests project in the Solution Explorer and choose Add > New Item.

In the Add New Item window select Class and name it WhileProjectingBookings.cs.

You add a method to it that describes the scenario GivenBookingProcessStarted_WhenProjectingBooking_ThenBookingShouldHaveStatusStarted

public class WhileProjectingBookings
{
    [Fact]
    public void GivenBookingProcessStarted_WhenProjectingBooking_ThenBookingShouldHaveStatusStarted()
    {

    }
}

You implement the test by first setting up the context for the scenario.

In this case, only a BookingStarted event has been emitted before.

You use the ProjectionInvoker helper class, from the MessageHandler libraries, to replay the history through the projection.

The expected status of the booking instance is Pending

[Fact]
public void GivenBookingProcessStarted_WhenProjectingBooking_ThenBookingShouldHaveStatusStarted()
{
    // given
    var history = new SourcedEvent[]
    {
        new BookingStarted
        {
            PurchaseOrder = new PurchaseOrder()
        }
    };
    var booking = new Booking();

    // when
    var invoker = new ProjectionInvoker(new BookingProjection());
    invoker.Invoke(booking, history);

    // then
    Assert.Equal("Pending", booking.Status);
}

The code does not compile yet thoug, there are a few classes that need to be implemented first.

Implementing the read model

The first class that needs to be implemented is called Booking.

This class is not part of the domain model, but it is part of a read model.

Which only serves the purpose of being human readable and nothing more.

Create a project for the read model

You create a new Visual Studio project to hold this model.

Right click the Solution MessageHandler.LearningPath in the Solution Explorer and choose Add > New Project.

Select the project template called Class Library, tagged with C#. Click Next.

Enter project name OrderBooking.Projections. Click Next.

Select framework .NET 6.0 (Long-term support). Click Create

Add a package reference to MessageHandler.EventSourcing

Right click on Dependencies of the OrderBooking.Projections project in the Solution Explorer and choose Manage Nuget Packages.

In the Nuget Package Manager window, browse for MessageHandler.EventSourcing (include prerelease needs to be checked).

Select the latest version and click Install.

Click I Accept in the License Acceptance window.

Add a project reference from the unit tests to the read model

Right click on Dependencies of the OrderBooking.UnitTests project in the Solution Explorer and choose Add Project Reference.

In the Reference Manager window, select OrderBooking.Projections. Click OK

Coding the read model

Now you rename the Class1.cs file in the OrderBooking.Projections project to Booking.cs.

Visual Studio will likely suggest to rename the class Class1 to Booking in the project automatically, you can allow it to do so. Otherwise you can change the class name manually.

In the Booking class you add a property called Status of type string.

public class Booking
{
    public string? Status { get; set; }
}

The code still does not compile at this point, the BookingProjection class is still missing.

Right click on the OrderBooking.Projections project in the Solution Explorer and choose Add > New Item.

In the Add New Item window select Class and name it BookingProjection.cs, you make the class public.

public class BookingProjection
{

}

Now the code compiles, you try to run the tests.

Right click the OrderBooking.UnitTests project in the Solution Explorer and choose Run Tests.

The test FAILS with message

Assert.Equal() Failure
Expected: Pending
Actual: (null)

The reason for this failure is that you have not implemented the projection logic yet.

Implementing the projection logic

To implement projection logic, you need to tell the projection what to do for each event type in the history and for each target type being projected into.

You do this by implementing the interface IProjection<TTarget, TEvent>.

For example, when BookingStarted gets projected onto Booking, you want to set the Status property of the Booking instance to Pending.

using MessageHandler.EventSourcing.Projections;
using OrderBooking.Events;

public class BookingProjection :
    IProjection<Booking, BookingStarted>
{
    public void Project(Booking booking, BookingStarted msg)
    {
        booking.Status = "Pending";
    }
}

When you now rerun the tests, you'll notice they will now SUCCEED.

Extending the projection

When the seller would have confirmed the booking before the buyer got to see the booking, it's status should turn to Confirmed.

Writing another test

You create another test to reflect this addition to the scenario.

[Fact]
public void GivenSalesOrderConfirmed_WhenProjectingBooking_ThenBookingShouldHaveStatusConfirmed()
{
    // given
    var history = new SourcedEvent[]
    {
        new BookingStarted
        {
            PurchaseOrder = new PurchaseOrder()
        },
        new SalesOrderConfirmed { }
    };
    var booking = new Booking();

    // when
    var invoker = new ProjectionInvoker(new BookingProjection());
    invoker.Invoke(booking, history);

    // then
    Assert.Equal("Confirmed", booking.Status);
}

To make this code compile, you add a new event SalesOrderConfirmed to OrderBooking.Events

Add the SalesOrderConfirmed event

Right click on the OrderBooking.Projections project in the Solution Explorer and choose Add > New Item.

In the Add New Item window select Class and name it SalesOrderConfirmed.cs

You make the class public and inherit it from SourcedEvent.

public class SalesOrderConfirmed : SourcedEvent
{
}

The code now compiles, so you run the test and notice it FAILS, with message

Assert.Equal() Failure
Expected: Confirmed
Actual: Pending

Implementing the additional projection logic

To make the test run, you need to add additional projection logic, to map the SalesOrderConfirmed event to the Booking instance.

public class BookingProjection :
        IProjection<Booking, BookingStarted>,
        IProjection<Booking, SalesOrderConfirmed>
{
    public void Project(Booking booking, BookingStarted msg)
    {
        booking.Status = "Pending";
    }

    public void Project(Booking booking, SalesOrderConfirmed msg)
    {
        booking.Status = "Confirmed";
    }
}

When you now run the tests again, they will SUCCEED

Congratulations! You made it to the end of lesson 4.

The code created up until now can also be found in the following commit of the reference repository.

Summary

In this lesson you learned how to build a Projection, a very common processing patterns used to summarize past decissions in distributed software.

What’s coming next?

So far the lifecycle of decissions taken was limited to the lifetime of a test run, after the test run they were discarded.

In Part 5 of the Tutorial, you’ll set up the infrastructure to persist events, so that your system can get a long term memory and recall all the decissions it has taken in the past.

TO PART 5

Sign up to our newsletter to get notified about new content and releases

You can unsubscribe at any time by clicking the link in the footer of your emails. I use Mailchimp as my marketing platform. By clicking subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.