Skip to content

HelloWorldProjection

Yves Reynhout edited this page Jun 3, 2016 · 7 revisions

Introduction

This page helps you write your first Hello World projection, explaining the process step by step.

Step 1 - Messages

Before you can write projections, you need to define the messages you are going to project. How you define them depends on a lot of things: preference, serialization framework in use, to mutate or to be immutable, etc ... Regardless, for simplicity, we'll define our first message as follows.

public class PeopleWhoSaidHello
{
  public readonly string[] People;

  public PeopleWhoSaidHello(string[] people)
  {
    if (people == null) 
      throw new ArgumentNullException("people");

    People = people;
  }
}

This message captures a list of people, their names, that said hello. Pretty useless in and by itself, but useful enough for a Hello World.

Step 2: Projection Declaration

How we define our projection depends on the data store we target. For simplicity sake, we're going to use the Console as our data store and print a Hello World line for each person who said hello.

public class HelloWorldProjection : ConnectedProjection<TextWriter>
{
  public HelloWorldProjection()
  {
    When<PeopleWhoSaidHello>((console, message) => 
    {
       foreach(var person in message.People)
         console.WriteLine("Hello World from {0}", person);
    }
  }
}

Here, we're declaring that, whenever we project a PeopleWhoSaidHello message and we pass it a TextWriter and the message instance, we'll write Hello World from ... for each person in that message. We're using Projac.Connector, which contains the ConnectedProjection<TConnection> abstract class and the boilerplate When<TMessage> method. In essence, this is equivalent to registering a callback method. We call these type of methods projection handlers.

Step 3: Projection Execution

Now that we've declared the projection, we can turn our attention on how to execute it. Assuming we're defining this code in a Console Application, we can write the following code in our Program.cs.

using System;
using System.Threading.Tasks;
using Projac.Connector;

namespace HelloWorldFromProjac
{
  public class Program
  {
    public static void Main()
    {
      MainAsync().Wait();
    }

    public static async Task MainAsync()
    {
      var projection = new HelloWorldProjection();
      var projector = new ConnectedProjector<TextWriter>(
        Resolve.WhenEqualToHandlerMessageType(projection.Handlers));
      
      var message = new PeopleWhoSaidHello(new [] { "John", "Jane" } );
      return projector.ProjectAsync(Console.Out, message);
    }
  }
}

The result of running this program gives us the following output:

Hello World from John
Hello World from Jane

Because Projac.Connector is async only, the Main method invokes a MainAsync method and waits for it to return. Inside MainAsync we define a new instance of our projection and wire it up to a projector, which will take care of dispatching messages to the appropriate projection handlers (callback methods). The projector doesn't actually take a dependency on the projection. Instead it uses a resolver which plays a role similar to an IoC container, namely resolving projection handlers. Since we only have one projection, the resolver only returns projection handlers registered with our projection. In this case, once the ProjectAsync method is called the projector will ask the resolver for the matching projection handler(s) and invoke them using the passed in Console and message. Here matching means that the type of the message must be an exact match with the projection handler's message type - this is achieved by using the WhenEqualToHandlerMessageType built-in resolver. Also note that Console.Out in the .NET BCL is a TextWriter.

Conclusion

Authoring projections is a fairly easy exercise. Projac tries to keep the boilerplate out and makes you concentrate on the parts that matter, which is figuring out what events to subscribe to and how to transform them into something useful.