This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Tutorials

Tutorials for the Dolittle platform

1 - Getting started

Get started with the Dolittle platform

Welcome to the tutorial for Dolittle, where you learn how to write a Microservice that keeps track of foods prepared by the chefs.

After this tutorial you will have:

Use the tabs to switch between the C# and TypeScript code examples. Full tutorial code available on GitHub for C# and TypeScript.

For a deeper dive into our Runtime, check our overview.

Setup

This tutorial expects you to have a basic understanding of C#, .NET and Docker.

Prerequisites:

Setup a .NET Core console project:

$ dotnet new console
$ dotnet add package Dolittle.SDK 

This tutorial expects you to have a basic understanding of TypeScript, npm and Docker.

Prerequisites:

Setup a TypeScript NodeJS project using your favorite package manager. For this tutorial we use npm.

$ npm init
$ npm -D install typescript ts-node
$ npm install @dolittle/sdk
$ npx tsc --init --experimentalDecorators --target es6

Create an EventType

First we’ll create an EventType that represents that a dish has been prepared. Events represents changes in the system, a “fact that has happened”. As the event “has happened”, it’s immutable by definition, and we should name it in the past tense accordingly.

An EventType is a class that defines the properties of the event. It acts as a wrapper for the type of the event.

// DishPrepared.cs
using Dolittle.SDK.Events;

[EventType("1844473f-d714-4327-8b7f-5b3c2bdfc26a")]
public class DishPrepared
{
    public DishPrepared (string dish, string chef)
    {
        Dish = dish;
        Chef = chef;
    }

    public string Dish { get; }
    public string Chef { get; }
}

The GUID given in the [EventType()] attribute is the EventTypeId, which is used to identify this EventType type in the Runtime.

// DishPrepared.ts
import { eventType } from '@dolittle/sdk.events';

@eventType('1844473f-d714-4327-8b7f-5b3c2bdfc26a')
export class DishPrepared {
    constructor(readonly Dish: string, readonly Chef: string) {}
}

The GUID given in the @eventType() decorator is the EventTypeId, which is used to identify this EventType in the Runtime.

Create an EventHandler

Now we need something that can react to dishes that have been prepared. Let’s create an EventHandler which prints the prepared dishes to the console.

// DishHandler.cs
using Dolittle.SDK.Events;
using Dolittle.SDK.Events.Handling;
using Microsoft.Extensions.Logging;

[EventHandler("f2d366cf-c00a-4479-acc4-851e04b6fbba")]
public class DishHandler
{
    readonly ILogger _logger;

    public DishHandler(ILogger<DishHandler> logger)
    {
        _logger = logger;
    }

    public void Handle(DishPrepared @event, EventContext eventContext)
    {
        _logger.LogInformation("{Chef} has prepared {Dish}. Yummm!", @event.Chef, @event.Dish);
    }
}

When an event is committed, the Handle() method will be called for all the EventHandlers that handle that EventType.

The [EventHandler()] attribute identifies this event handler in the Runtime, and is used to keep track of which event it last processed, and retrying the handling of an event if the handler fails (throws an exception).

// DishHandler.ts
import { inject } from '@dolittle/sdk.dependencyinversion';
import { EventContext } from '@dolittle/sdk.events';
import { eventHandler, handles } from '@dolittle/sdk.events.handling';
import { Logger } from 'winston';

import { DishPrepared } from './DishPrepared';

@eventHandler('f2d366cf-c00a-4479-acc4-851e04b6fbba')
export class DishHandler {
    constructor(
        @inject('Logger') private readonly _logger: Logger
    ) {}

    @handles(DishPrepared)
    dishPrepared(event: DishPrepared, eventContext: EventContext) {
        this._logger.info(`${event.Chef} has prepared ${event.Dish}. Yummm!`);
    }
}

When an event is committed, the method decorated with the @handles(EventType) for that specific EventType will be called.

The @eventHandler() decorator identifies this event handler in the Runtime, and is used to keep track of which event it last processed, and retrying the handling of an event if the handler fails (throws an exception).

Connect the client and commit an event

Let’s build a client that connects to the Runtime for a Microservice with the id "f39b1f61-d360-4675-b859-53c05c87c0e6". This sample Microservice is pre-configured in the development Docker image.

While configuring the client we register the EventTypes and EventHandlers so that the Runtime knows about them. Then we can prepare a delicious taco and commit it to the EventStore for the specified tenant.

// Program.cs
using Dolittle.SDK;
using Dolittle.SDK.Tenancy;
using Microsoft.Extensions.Hosting;

var host = Host.CreateDefaultBuilder()
    .UseDolittle()
    .Build();

await host.StartAsync();

var client = await host.GetDolittleClient();
await client.EventStore
    .ForTenant(TenantId.Development)
    .CommitEvent(new DishPrepared("Bean Blaster Taco", "Mr. Taco"), "Dolittle Tacos");

await host.WaitForShutdownAsync();

The string given in FromEventSource() is the EventSourceId, which is used to identify where the events come from.

// index.ts
import { DolittleClient } from '@dolittle/sdk';
import { TenantId } from '@dolittle/sdk.execution';

import './DishHandler';
import { DishPrepared } from './DishPrepared';

(async () => {
    const client = await DolittleClient
        .setup()
        .connect();

    const preparedTaco = new DishPrepared('Bean Blaster Taco', 'Mr. Taco');

    await client.eventStore
        .forTenant(TenantId.development)
        .commit(preparedTaco, 'Dolittle Tacos');
})();

The string given in the commit() call is the EventSourceId, which is used to identify where the events come from.

Start the Dolittle environment

Start the Dolittle environment with all the necessary dependencies with the following command:

$ docker run -p 50053:50053 -p 51052:51052 -p 27017:27017 -d dolittle/runtime:latest-development

This will start a container with the Dolittle Development Runtime on port 50053 and 51052 and a MongoDB server on port 27017. The Runtime handles committing the events and the event handlers while the MongoDB is used for persistence.

Run your microservice

Run your code, and get a delicious serving of taco:

$ dotnet run
info: Dolittle.SDK.DolittleClientService[0]
      Connecting Dolittle Client
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: .../GettingStarted
info: Dolittle.SDK.Events.Processing.EventProcessors[0]
      EventHandler f2d366cf-c00a-4479-acc4-851e04b6fbba registered with the Runtime, start handling requests
info: DishHandler[0]
      Mr. Taco has prepared Bean Blaster Taco. Yummm!

$ npx ts-node index.ts
info: EventHandler f2d366cf-c00a-4479-acc4-851e04b6fbba registered with the Runtime, start handling requests.
info: Mr. Taco has prepared Bean Blaster Taco. Yummm!

Check the status of your microservice

With everything is up and running you can use the Dolittle CLI to check what’s going on.

Open a new terminal.

Now you can list the registered event types with the following command:

$ dolittle runtime eventtypes list
EventType   
------------
DishPrepared

And check the status of the event handler with the following commands:

$ dolittle runtime eventhandlers list
EventHandler  Scope    Partitioned  Status
------------------------------------------
DishHandler   Default  ✅            ✅ 

$ dolittle runtime eventhandlers get DishHandler
Tenant                                Position  Status
------------------------------------------------------
445f8ea8-1a6f-40d7-b2fc-796dba92dc44  1

What’s next

2 - Aggregates

Get started with Aggregates

Welcome to the tutorial for Dolittle, where you learn how to write a Microservice that keeps track of foods prepared by the chefs.

After this tutorial you will have:

  • a running Dolittle environment with a Runtime and a MongoDB,
  • a Microservice that commits and handles Events and
  • a stateful aggregate root that applies events and is controlled by an invariant

Use the tabs to switch between the C# and TypeScript code examples. Full tutorial code available on GitHub for C# and TypeScript.

Pre requisites

This tutorial builds directly upon and that you have gone through our getting started guide; done the setup, created the EventType, EventHandler and connected the client

Create an AggregateRoot

An aggregate root is a class that upholds the rules (invariants) in your domain. An aggregate root is responsible for deciding which events should be committed. It exposes public methods that represents actions to be performed, and holds internal state to decide if the action is allowed.

Before one of the public methods is called, the internal state is rehydrated by calling the On-methods for all the events the aggregate root has already applied. These On-methods updates the internal state of the aggregate root, and must not have any other side-effects. When a public action method is executed, it can use this internal state decide either to apply events to be committed, or throw an error if the action is not allowed.

The following code implements an aggregate root for a Kitchen that only has enough ingredients to prepare two dishes:

// Kitchen.cs
using System;
using Dolittle.SDK.Aggregates;
using Dolittle.SDK.Events;

[AggregateRoot("01ad9a9f-711f-47a8-8549-43320f782a1e")]
public class Kitchen : AggregateRoot
{
    int _ingredients = 2;

    public Kitchen(EventSourceId eventSource)
        : base(eventSource)
    {
    }

    public void PrepareDish(string dish, string chef)
    {
        if (_ingredients <= 0) throw new Exception("We have run out of ingredients, sorry!");
        Apply(new DishPrepared(dish, chef));
        Console.WriteLine($"Kitchen {EventSourceId} prepared a {dish}, there are {_ingredients} ingredients left.");
    }

    void On(DishPrepared @event)
        => _ingredients--;
}

The GUID given in the [AggregateRoot()] attribute is the AggregateRootId, which is used to identify this AggregateRoot in the Runtime.

// Kitchen.ts
import { aggregateRoot, AggregateRoot, on } from '@dolittle/sdk.aggregates';
import { EventSourceId } from '@dolittle/sdk.events';

import { DishPrepared } from './DishPrepared';

@aggregateRoot('01ad9a9f-711f-47a8-8549-43320f782a1e')
export class Kitchen extends AggregateRoot {
    private _ingredients: number = 2;

    constructor(eventSourceId: EventSourceId) {
        super(eventSourceId);
    }

    prepareDish(dish: string, chef: string) {
        if (this._ingredients <= 0) throw new Error('We have run out of ingredients, sorry!');
        this.apply(new DishPrepared(dish, chef));
        console.log(`Kitchen ${this.eventSourceId} prepared a ${dish}, there are ${this._ingredients} ingredients left.`);
    }

    @on(DishPrepared)
    onDishPrepared(event: DishPrepared) {
        this._ingredients--;
    }
}

The GUID given in the @aggregateRoot() decorator is the AggregateRootId, which is used to identify this AggregateRoot in the Runtime.

Apply the event through an aggregate of the Kitchen aggregate root

Let’s expand upon the client built in the getting started guide. But instead of committing the event to the event store directly we perform an action on the aggregate that eventually applies and commits the event.

// Program.cs
using Dolittle.SDK;
using Dolittle.SDK.Tenancy;
using Microsoft.Extensions.Hosting;

var host = Host.CreateDefaultBuilder()
    .UseDolittle()
    .Build();

await host.StartAsync();

var client = await host.GetDolittleClient();

await client.Aggregates
    .ForTenant(TenantId.Development)
    .Get<Kitchen>("Dolittle Tacos")
    .Perform(kitchen => kitchen.PrepareDish("Bean Blaster Taco", "Mr. Taco"));

await host.WaitForShutdownAsync();

The string given in AggregateOf<Kitchen>() is the EventSourceId, which is used to identify the aggregate of the aggregate root to perform the action on.

Note that we also register the aggregate root class on the client builder using .WithAggregateRoots(...).

// index.ts
import { DolittleClient } from '@dolittle/sdk';
import { TenantId } from '@dolittle/sdk.execution';

import  './DishHandler';
import { Kitchen } from './Kitchen';

(async () => {
    const client = await DolittleClient
        .setup()
        .connect();

    await client.aggregates
        .forTenant(TenantId.development)
        .get(Kitchen, 'Dolittle Tacos')
        .perform(kitchen => kitchen.prepareDish('Bean Blaster Taco', 'Mr. Taco'));
})();

The string given in the aggregateOf() call is the EventSourceId, which is used to identify the aggregate of the aggregate root to perform the action on.

Note that we also register the aggregate root class on the client builder using .withAggregateRoots(...).

Start the Dolittle environment

If you don’t have a Runtime already going from a previous tutorial, start the Dolittle environment with all the necessary dependencies with the following command:

$ docker run -p 50053:50053 -p 51052:51052 -p 27017:27017 -d dolittle/runtime:latest-development

This will start a container with the Dolittle Development Runtime on port 50053 and 51052 and a MongoDB server on port 27017. The Runtime handles committing the events and the event handlers while the MongoDB is used for persistence.

Run your microservice

Run your code twice, and get a two delicious servings of taco:

$ dotnet run
info: Dolittle.SDK.DolittleClientService[0]
      Connecting Dolittle Client
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: .../Aggregates
info: Dolittle.SDK.Events.Processing.EventProcessors[0]
      EventHandler f2d366cf-c00a-4479-acc4-851e04b6fbba registered with the Runtime, start handling requests
Kitchen Dolittle Tacos prepared a Bean Blaster Taco, there are 1 ingredients left.
info: DishHandler[0]
      Mr. Taco has prepared Bean Blaster Taco. Yummm!


$ dotnet run
info: Dolittle.SDK.DolittleClientService[0]
      Connecting Dolittle Client
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: .../Aggregates
info: Dolittle.SDK.Events.Processing.EventProcessors[0]
      EventHandler f2d366cf-c00a-4479-acc4-851e04b6fbba registered with the Runtime, start handling requests
Kitchen Dolittle Tacos prepared a Bean Blaster Taco, there are 0 ingredients left.
info: DishHandler[0]
      Mr. Taco has prepared Bean Blaster Taco. Yummm!

$ npx ts-node index.ts
info: EventHandler f2d366cf-c00a-4479-acc4-851e04b6fbba registered with the Runtime, start handling requests.
Kitchen Dolittle Tacos prepared a Bean Blaster Taco, there are 1 ingredients left.
info: Mr. Taco has prepared Bean Blaster Taco. Yummm!

$ npx ts-node index.ts
info: EventHandler f2d366cf-c00a-4479-acc4-851e04b6fbba registered with the Runtime, start handling requests.
Kitchen Dolittle Tacos prepared a Bean Blaster Taco, there are 0 ingredients left.
info: Mr. Taco has prepared Bean Blaster Taco. Yummm!

Check the status of your Kitchen aggregate root

Open a new terminal for the Dolittle CLI and run the following commands:

$ dolittle runtime aggregates list
AggregateRoot  Instances
------------------------
Kitchen        1

$ dolittle runtime aggregates get Kitchen --wide
Tenant                                EventSource     AggregateRootVersion
--------------------------------------------------------------------------
445f8ea8-1a6f-40d7-b2fc-796dba92dc44  Dolittle Tacos  2

$ dolittle runtime aggregates events Kitchen "Dolittle Tacos" --wide
AggregateRootVersion  EventLogSequenceNumber  EventType     Public  Occurred                  
----------------------------------------------------------------------------------------------
0                     0                       DishPrepared  False   11/04/2021 14:04:19 +00:00
1                     1                       DishPrepared  False   11/04/2021 14:04:37 +00:00

Try to prepare a dish without any ingredients

Since we have already used up all the available ingredients, the Kitchen aggregate root should not allow us to prepare any more dishes. Run your code a third time, and you will see that the exception gets thrown from the aggregate root.

$ dotnet run
info: Dolittle.SDK.DolittleClientService[0]
      Connecting Dolittle Client
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: .../Aggregates
info: Dolittle.SDK.Events.Processing.EventProcessors[0]
      EventHandler f2d366cf-c00a-4479-acc4-851e04b6fbba registered with the Runtime, start handling requests
Unhandled exception. System.Exception: We have run out of ingredients, sorry!
... stack trace ...

$ npx ts-node index.ts
info: EventHandler f2d366cf-c00a-4479-acc4-851e04b6fbba registered with the Runtime, start handling requests.

.../Kitchen.ts:20
        if (this._ingredients <= 0) throw new Error('We have run out of ingredients, sorry!');
                                          ^
Error: We have run out of ingredients, sorry!
... stack trace ...

You can verify that the Kitchen did not allow a third dish to be prepared, by checking the committed events:

$ dolittle runtime aggregates events Kitchen "Dolittle Tacos" --wide
AggregateRootVersion  EventLogSequenceNumber  EventType     Public  Occurred                  
----------------------------------------------------------------------------------------------
0                     0                       DishPrepared  False   11/04/2021 14:04:19 +00:00
1                     1                       DishPrepared  False   11/04/2021 14:04:37 +00:00

Events from aggregate roots are just normal events

The events applied (committed) from aggregate roots are handled the same way as events committed directly to the event store. You can verify this by checking the status of the DishHandler:

$ dolittle runtime eventhandlers get DishHandler
Tenant                                Position  Status
------------------------------------------------------
445f8ea8-1a6f-40d7-b2fc-796dba92dc44  2

What’s next

3 - Projections

Get started with Projections

Welcome to the tutorial for Projections in Dolittle, where you learn how to write a Microservice that keeps track of food prepared by the chefs. After this tutorial you will have:

Use the tabs to switch between the C# and TypeScript code examples. Full tutorial code available on GitHub for C# and TypeScript.

Setup

This tutorial builds directly upon the getting started guide and the files from the it.

Prerequisites:

Before getting started, your directory should look something like this:

└── Projections/
    ├── DishHandler.cs
    ├── DishPrepared.cs
    ├── Program.cs
    └── Projections.csproj

Prerequisites:

Before getting started, your directory should look something like this:

└── projections/
    ├── .eslintrc
    ├── DishHandler.ts
    ├── DishPrepared.ts
    ├── index.ts
    ├── package.json
    └── tsconfig.json

Start the Dolittle environment

If you don’t have a Runtime already going from a previous tutorial, start the Dolittle environment with all the necessary dependencies with the following command:

$ docker run -p 50053:50053 -p 51052:51052 -p 27017:27017 -d dolittle/runtime:latest-development

This will start a container with the Dolittle Development Runtime on port 50053 and 51052 and a MongoDB server on port 27017. The Runtime handles committing the events and the event handlers while the MongoDB is used for persistence.

Create a DishCounter Projection

First, we’ll create a Projection that keeps track of the dishes and how many times the chefs have prepared them. Projections are a special type of event handler that mutate a read model based on incoming events.

// DishCounter.cs
using Dolittle.SDK.Projections;

[Projection("98f9db66-b6ca-4e5f-9fc3-638626c9ecfa")]
public class DishCounter
{
    public string Name = "Unknown";
    public int NumberOfTimesPrepared = 0;

    [KeyFromProperty("Dish")]
    public void On(DishPrepared @event, ProjectionContext context)
    {
        Name = @event.Dish;
        NumberOfTimesPrepared ++;
    }
}

The [Projection()] attribute identifies this Projection in the Runtime, and is used to keep track of the events that it processes, and retrying the handling of an event if the handler fails (throws an exception). If the Projection is changed somehow (eg. a new On() method or the EventType changes), it will replay all of its events.

When an event is committed, the On() method is called for all the Projections that handle that EventType. The attribute [KeyFromEventProperty()] defines what property on the event will be used as the read model’s key (or id). You can choose the [KeyFromEventSource], [KeyFromPartitionId] or a property from the event with [KeyFromEventProperty(@event => @event.Property)].

// DishCounter.ts
import { ProjectionContext, projection, on } from '@dolittle/sdk.projections';

import { DishPrepared } from './DishPrepared';

@projection('98f9db66-b6ca-4e5f-9fc3-638626c9ecfa')
export class DishCounter {
    name: string = 'Unknown';
    numberOfTimesPrepared: number = 0;

    @on(DishPrepared, _ => _.keyFromProperty('Dish'))
    on(event: DishPrepared, projectionContext: ProjectionContext) {
        this.name = event.Dish;
        this.numberOfTimesPrepared ++;
    }
}

The @projection() decorator identifies this Projection in the Runtime, and is used to keep track of the events that it processes, and retrying the handling of an event if the handler fails (throws an exception). If the Projection is changed somehow (eg. a new @on() decorator or the EventType changes), it will replay all of it’s events.

When an event is committed, the method decoratored with @on() will be called for all the Projections that handle that EventType. The second parameter in the @on decorator is a callback function, that defines what property on the event will be used as the read model’s key (or id). You can choose either _ => _.keyFromEventSource(), _ => _.keyFromPartitionId() or a property from the event with _ => _.keyFromProperty('propertyName').

Register and get the DishCounter Projection

Let’s register the projection, commit new DishPrepared events and get the projection to see how it reacted.

// Program.cs
using System;
using System.Threading.Tasks;
using Dolittle.SDK;
using Dolittle.SDK.Tenancy;
using Microsoft.Extensions.Hosting;

var host = Host.CreateDefaultBuilder()
    .UseDolittle()
    .Build();
await host.StartAsync();

var client = await host.GetDolittleClient();
var eventStore = client.EventStore.ForTenant(TenantId.Development);
await eventStore.CommitEvent(new DishPrepared("Bean Blaster Taco", "Mr. Taco"), "Dolittle Tacos");
await eventStore.CommitEvent(new DishPrepared("Bean Blaster Taco", "Mrs. Tex Mex"), "Dolittle Tacos");
await eventStore.CommitEvent(new DishPrepared("Avocado Artillery Tortilla", "Mr. Taco"), "Dolittle Tacos");
await eventStore.CommitEvent(new DishPrepared("Chili Canon Wrap", "Mrs. Tex Mex"), "Dolittle Tacos");

await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);

var dishes = await client.Projections
    .ForTenant(TenantId.Development)
    .GetAll<DishCounter>().ConfigureAwait(false);

foreach (var dish in dishes)
{
    Console.WriteLine($"The kitchen has prepared {dish.Name} {dish.NumberOfTimesPrepared} times");
}

await host.WaitForShutdownAsync();

The GetAll<DishCounter>() method returns all read models of that Projection as an IEnumerable<DishCounter>.

The string given in FromEventSource() is the EventSourceId, which is used to identify where the events come from.

// index.ts
import { DolittleClient } from '@dolittle/sdk';
import { TenantId } from '@dolittle/sdk.execution';
import { setTimeout } from 'timers/promises';

import { DishCounter } from './DishCounter';
import { DishPrepared } from './DishPrepared';

(async () => {
    const client = await DolittleClient
        .setup()
        .connect();

    const eventStore = client.eventStore.forTenant(TenantId.development);

    await eventStore.commit(new DishPrepared('Bean Blaster Taco', 'Mr. Taco'), 'Dolittle Tacos');
    await eventStore.commit(new DishPrepared('Bean Blaster Taco', 'Mrs. Tex Mex'), 'Dolittle Tacos');
    await eventStore.commit(new DishPrepared('Avocado Artillery Tortilla', 'Mr. Taco'), 'Dolittle Tacos');
    await eventStore.commit(new DishPrepared('Chili Canon Wrap', 'Mrs. Tex Mex'), 'Dolittle Tacos');

    await setTimeout(1000);

    for (const { name, numberOfTimesPrepared } of await client.projections.forTenant(TenantId.development).getAll(DishCounter)) {
        client.logger.info(`The kitchen has prepared ${name} ${numberOfTimesPrepared} times`);
    }
})();

The getAll(DishCounter) method returns all read models for that Projection as an array DishCounter[].

The string given in commit(event, 'event-source-id') is the EventSourceId, which is used to identify where the events come from.

Run your microservice

Run your code, and see the different dishes:

$ dotnet run
info: Dolittle.SDK.DolittleClientService[0]
      Connecting Dolittle Client
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: .../Projections
info: Dolittle.SDK.Events.Processing.EventProcessors[0]
      Projection 98f9db66-b6ca-4e5f-9fc3-638626c9ecfa registered with the Runtime, start handling requests
The kitchen has prepared Bean Blaster Taco 2 times
The kitchen has prepared Avocado Artillery Tortilla 1 times
The kitchen has prepared Chili Canon Wrap 1 times

$ npx ts-node index.ts
info: Projection 98f9db66-b6ca-4e5f-9fc3-638626c9ecfa registered with the Runtime, start handling requests.
info: The kitchen has prepared Bean Blaster Taco 2 times
info: The kitchen has prepared Avocado Artillery Tortilla 1 times
info: The kitchen has prepared Chili Canon Wrap 1 times

Add Chef read model

Let’s add another read model to keep track of all the chefs and . This time let’s only create the class for the read model:

// Chef.cs
using System.Collections.Generic;

public class Chef
{
    public string Name = "";
    public List<string> Dishes = new();
}

// Chef.ts
export class Chef {
    constructor(
        public name: string = '',
        public dishes: string[] = []
    ) { }
}

Create and get the inline projection for Chef read model

You can also create a Projection inline in the client building steps instead of declaring a class for it.

Let’s create an inline Projection for the Chef read model:

// Program.cs
using System;
using System.Threading.Tasks;
using Dolittle.SDK;
using Dolittle.SDK.Tenancy;
using Microsoft.Extensions.Hosting;

var host = Host.CreateDefaultBuilder()
    .UseDolittle(_ => _ 
        .WithProjections(_ => _
            .Create("0767bc04-bc03-40b8-a0be-5f6c6130f68b")
                .ForReadModel<Chef>()
                .On<DishPrepared>(_ => _.KeyFromProperty(_ => _.Chef), (chef, @event, projectionContext) =>
                {
                    chef.Name = @event.Chef;
                    if (!chef.Dishes.Contains(@event.Dish)) chef.Dishes.Add(@event.Dish);
                    return chef;
                })
        )
    )
    .Build();
await host.StartAsync();

var client = await host.GetDolittleClient();
var eventStore = client.EventStore.ForTenant(TenantId.Development);
await eventStore.CommitEvent(new DishPrepared("Bean Blaster Taco", "Mr. Taco"), "Dolittle Tacos");
await eventStore.CommitEvent(new DishPrepared("Bean Blaster Taco", "Mrs. Tex Mex"), "Dolittle Tacos");
await eventStore.CommitEvent(new DishPrepared("Avocado Artillery Tortilla", "Mr. Taco"), "Dolittle Tacos");
await eventStore.CommitEvent(new DishPrepared("Chili Canon Wrap", "Mrs. Tex Mex"), "Dolittle Tacos");

await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);

var dishes = await client.Projections
    .ForTenant(TenantId.Development)
    .GetAll<DishCounter>().ConfigureAwait(false);

foreach (var dish in dishes)
{
    Console.WriteLine($"The kitchen has prepared {dish.Name} {dish.NumberOfTimesPrepared} times");
}

var chef = await client.Projections
    .ForTenant(TenantId.Development)
    .Get<Chef>("Mrs. Tex Mex").ConfigureAwait(false);
Console.WriteLine($"{chef.Name} has prepared {string.Join(", ", chef.Dishes)}");

await host.WaitForShutdownAsync();

The Get<Chef>('key') method returns a Projection instance with that particular key. The key is declared by the KeyFromProperty(_.Chef) callback function on the On() method. In this case, the key of each Chef projection instance is based on the chefs name.

// index.ts
(async () => {
    const client = await DolittleClient
        .setup(builder => builder
            .withProjections(_ => _
                .create('0767bc04-bc03-40b8-a0be-5f6c6130f68b')
                    .forReadModel(Chef)
                    .on(DishPrepared, _ => _.keyFromProperty('Chef'), (chef, event, projectionContext) => {
                        chef.name = event.Chef;
                        if (!chef.dishes.includes(event.Dish)) chef.dishes.push(event.Dish);
                        return chef;
                    })
            )
        )
        .connect();

    const eventStore = client.eventStore.forTenant(TenantId.development);

    await eventStore.commit(new DishPrepared('Bean Blaster Taco', 'Mr. Taco'), 'Dolittle Tacos');
    await eventStore.commit(new DishPrepared('Bean Blaster Taco', 'Mrs. Tex Mex'), 'Dolittle Tacos');
    await eventStore.commit(new DishPrepared('Avocado Artillery Tortilla', 'Mr. Taco'), 'Dolittle Tacos');
    await eventStore.commit(new DishPrepared('Chili Canon Wrap', 'Mrs. Tex Mex'), 'Dolittle Tacos');

    await setTimeout(1000);

    for (const { name, numberOfTimesPrepared } of await client.projections.forTenant(TenantId.development).getAll(DishCounter)) {
        client.logger.info(`The kitchen has prepared ${name} ${numberOfTimesPrepared} times`);
    }

    const chef = await client.projections.forTenant(TenantId.development).get(Chef, 'Mrs. Tex Mex');
    client.logger.info(`${chef.name} has prepared ${chef.dishes.join(', ')}`);
})();

The get(Chef, 'key') method returns a Projection instance with that particular key. The key is declared by the keyFromProperty('Chef') callback function on the on() method. In this case, the key of each Chef projection instance is based on the chefs name.

Run your microservice with the inline Chef projection

Run your code, and get a delicious serving of taco:

$ dotnet run
info: Dolittle.SDK.DolittleClientService[0]
      Connecting Dolittle Client
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: .../Projections
info: Dolittle.SDK.Events.Processing.EventProcessors[0]
      Projection 0767bc04-bc03-40b8-a0be-5f6c6130f68b registered with the Runtime, start handling requests
info: Dolittle.SDK.Events.Processing.EventProcessors[0]
      Projection 98f9db66-b6ca-4e5f-9fc3-638626c9ecfa registered with the Runtime, start handling requests
The kitchen has prepared Bean Blaster Taco 4 times
The kitchen has prepared Avocado Artillery Tortilla 2 times
The kitchen has prepared Chili Canon Wrap 2 times
Mrs. Tex Mex has prepared Bean Blaster Taco, Chili Canon Wrap

$ npx ts-node index.ts
info: Projection 0767bc04-bc03-40b8-a0be-5f6c6130f68b registered with the Runtime, start handling requests.
info: Projection 98f9db66-b6ca-4e5f-9fc3-638626c9ecfa registered with the Runtime, start handling requests.
info: The kitchen has prepared Bean Blaster Taco 4 times
info: The kitchen has prepared Avocado Artillery Tortilla 2 times
info: The kitchen has prepared Chili Canon Wrap 2 times
info: Mrs. Tex Mex has prepared Bean Blaster Taco,Chili Canon Wrap

What’s next

4 - Embeddings

Get started with Embeddings

Welcome to the tutorial for Embeddings in Dolittle, where you learn how to write a Microservice that event sources data coming from an external system. This external system is keeping track of the chefs in the kitchen, like a HR system. We’re going to be “freeing” this data by event sourcing with the help of Embeddings, so that other parts of the code can utilize it more freely.

Embeddings are like a combination of an Aggregate and a Projection. They have a collection of states (the read models) with unique keys (within the embedding). Whenever a new updated state is pushed/pulled from the external system, that updated state will be compared against its representative read model in the embedding. Whenever the states differ, the Runtime will call the embedding to resolve the difference into events. The embedding will then handle these events and modify its state to match the desired state.

The main point of embeddings is event source the changes coming from an external system. An embeddings read model exists so that we can commit the correct events and uphold its logic. Other event handlers, projections and microservices can then build upon these events.

Example of a embedding:

Diagram of embeddings

After this tutorial you will have:

  • a running Dolittle environment with a Runtime and a MongoDB, and
  • a Microservice that commits Events
  • an Embedding which creates events based on changes in the HR system.

Use the tabs to switch between the C# and TypeScript code examples. Full tutorial code available on GitHub for C# and TypeScript.

Setup

Setup is the same as in the getting started tutorial.

Prerequisites:

Before getting started, your directory should look something like this:

└── Kitchen/
    ├── Program.cs
    └── Kitchen.csproj

Prerequisites:

Before getting started, your directory should look something like this:

└── kitchen/
    ├── .eslintrc
    ├── index.ts
    ├── package.json
    └── tsconfig.json

Start the Dolittle environment

If you don’t have a Runtime already going from a previous tutorial, start the Dolittle environment with all the necessary dependencies with the following command:

$ docker run -p 50053:50053 -p 51052:51052 -p 27017:27017 -d dolittle/runtime:latest-development

This will start a container with the Dolittle Development Runtime on port 50053 and 51052 and a MongoDB server on port 27017. The Runtime handles committing the events and the event handlers while the MongoDB is used for persistence.

Create the events

In this example, we want to event source the data coming from a mocked HR system. We want to keep track of hired employees, their workplace and whenever they retire. Let’s create 3 different EventTypes to signal whenever an employee is hired, transferred or retires:

EmployeeHired:

// EmployeeHired.cs
using Dolittle.SDK.Events;

[EventType("8fdf45bc-f484-4348-bcb0-4d6f134aaf6c")]
public class EmployeeHired
{
    public string Name { get; set; }

    public EmployeeHired(string name) => Name = name;
}

EmployeeTransferred:

// EmployeeTransferred.cs
using Dolittle.SDK.Events;

[EventType("b27f2a39-a2d4-43a7-9952-62e39cbc7ebc")]
public class EmployeeTransferred
{
    public string Name { get; set; }
    public string From { get; set; }
    public string To { get; set; }

    public EmployeeTransferred(string name, string from, string to)
    {
        Name = name;
        From = from;
        To = to;
    }
}

EmployeeRetired:

// EmployeeRetired.cs
using Dolittle.SDK.Events;

[EventType("1932beb4-c8cd-4fee-9a7e-a92af3693510")]
public class EmployeeRetired
{
    public string Name { get; set; }

    public EmployeeRetired(string name) => Name = name;
}

EmployeeHired:

//EmployeeHired.ts
import { eventType } from '@dolittle/sdk.events';

@eventType('8fdf45bc-f484-4348-bcb0-4d6f134aaf6c')
export class EmployeeHired {
    constructor(readonly name: string) {}
}

EmployeeTransferred:

//EmployeeTransferred.ts
import { eventType } from '@dolittle/sdk.events';

@eventType('b27f2a39-a2d4-43a7-9952-62e39cbc7ebc')
export class EmployeeTransferred {
    constructor(readonly name: string, readonly from: string, readonly to: string) {}
}

EmployeeRetired:

//EmployeeRetired.ts
import { eventType } from '@dolittle/sdk.events';

@eventType('1932beb4-c8cd-4fee-9a7e-a92af3693510')
export class EmployeeRetired {
    constructor(readonly name: string) {}
}

Create an Employee Embedding

In this example, we want to events source the data coming from a mocked HR system by using Embeddings. Let’s create an Embedding that keeps track of the changes coming from the HR system by committing and handling of the events we made earlier:

// Employee.cs
using System;
using Dolittle.SDK.Embeddings;
using Dolittle.SDK.Projections;

[Embedding("e5577d2c-0de7-481c-b5be-6ef613c2fcd6")]
public class Employee
{
    public string Name { get; set; } = "";
    public string Workplace { get; set; } = "Unassigned";

    public object ResolveUpdateToEvents(Employee updatedEmployee, EmbeddingContext context)
    {
        if (Name != updatedEmployee.Name)
        {
            return new EmployeeHired(updatedEmployee.Name);
        }
        else if (Workplace != updatedEmployee.Workplace)
        {
            return new EmployeeTransferred(Name, Workplace, updatedEmployee.Workplace);
        }

        throw new NotImplementedException();
    }

    public object ResolveDeletionToEvents(EmbeddingContext context)
    {
        return new EmployeeRetired(Name);
    }

    public void On(EmployeeHired @event, EmbeddingProjectContext context)
    {
        Name = @event.Name;
    }

    public void On(EmployeeTransferred @event, EmbeddingProjectContext context)
    {
        Workplace = @event.To;
    }

    public ProjectionResultType On(EmployeeRetired @event, EmbeddingProjectContext context)
    {
        return ProjectionResultType.Delete;
    }
}

The [Embedding()] attribute identifies this embedding in the Runtime, and is used to keep track of the events that it creates and processes, it’s state and the retrying the handling of an event if the handler fails (throws an exception).

ResolveUpdateToEvents() method will be called whenever the current state of the embeddings read model is different from the updated state. This method needs to return one or many events that will update the read model so that it moves “closer” to matching the desired state. The Runtime will then apply the returned events onto the embeddings On() methods. If the states still differ, it will call the ResolveUpdateToEvents() method again until the read models current state matches the updated state. At that point, the events will be committed to the Event Log. If the On() methods fail, or the Runtime detects that the embeddings state is looping, the process will be stopped and no events will be committed. This means that events will only be committed if they successfully result in the states matching.

The ResolveDeletionToEvents() method is the same, except the resulting events have to result in the read model being deleted. This is done by returning a ProjectionResult<Employee>.Delete in the corresponding On() method.

The committed events are always public Aggregate events. The AggregateRootId is the same as the EmbeddingId, and the EventSourceId is the same as the read models key.

Unlike projections, you don’t need to specify a KeySelector for the On() methods.

// Employee.ts
import { CouldNotResolveUpdateToEvents, embedding, EmbeddingContext, EmbeddingProjectContext, on, resolveDeletionToEvents, resolveUpdateToEvents } from '@dolittle/sdk.embeddings';
import { ProjectionResult } from '@dolittle/sdk.projections';

import { EmployeeHired } from './EmployeeHired';
import { EmployeeRetired } from './EmployeeRetired';
import { EmployeeTransferred } from './EmployeeTransferred';

@embedding('e5577d2c-0de7-481c-b5be-6ef613c2fcd6')
export class Employee {

    constructor(
        public name: string = '',
        public workplace: string = 'Unassigned') {
    }

    @resolveUpdateToEvents()
    resolveUpdateToEvents(updatedEmployee: Employee, context: EmbeddingContext) {
        if (this.name !== updatedEmployee.name) {
            return new EmployeeHired(updatedEmployee.name);
        } else if (this.workplace !== updatedEmployee.workplace) {
            return new EmployeeTransferred(this.name, this.workplace, updatedEmployee.workplace);
        }

        throw new CouldNotResolveUpdateToEvents();
    }

    @resolveDeletionToEvents()
    resolveDeletionToEvents(context: EmbeddingContext) {
        return new EmployeeRetired(this.name);
    }

    @on(EmployeeHired)
    onEmployeeHired(event: EmployeeHired, context: EmbeddingProjectContext) {
        this.name = event.name;
    }

    @on(EmployeeTransferred)
    onEmployeeTransferred(event: EmployeeTransferred, context: EmbeddingProjectContext) {
        this.workplace = event.to;
    }

    @on(EmployeeRetired)
    onEmployeeRetired(event: EmployeeRetired, context: EmbeddingProjectContext) {
        return ProjectionResult.delete;
    }
}

Register the Employee embedding, and update and delete a read model

Let’s register the new event types and the embedding. Then we can update and delete a read model from it.

// Program.cs
using System;
using System.Threading.Tasks;
using Dolittle.SDK;
using Dolittle.SDK.Tenancy;
using Microsoft.Extensions.Hosting;

var host = Host.CreateDefaultBuilder()
    .UseDolittle()
    .Build();

await host.StartAsync();

var client = await host.GetDolittleClient();

var updatedEmployee = new Employee
{
    Name = "Mr. Taco",
    Workplace = "Street Food Taco Truck"
};

await Task.Delay(TimeSpan.FromSeconds(4));

await client.Embeddings
    .ForTenant(TenantId.Development)
    .Update(updatedEmployee.Name, updatedEmployee);
Console.WriteLine($"Updated {updatedEmployee.Name}.");

await client.Embeddings
    .ForTenant(TenantId.Development)
    .Delete<Employee>(updatedEmployee.Name);
Console.WriteLine($"Deleted {updatedEmployee.Name}.");

await host.WaitForShutdownAsync();

The Update() method tries to update the embeddings read model with the specified key to match the updated state by calling the embeddings ResolveUpdateToEvents() method. If no read model exists with the key, it will create one with the read model set to the embedding’s initial state.

The Delete() method will call the embeddings ResolveDeletionToEvents() for the specified key. This method then returns one or many events, which when handled will delete the read model.

// index.ts
import { DolittleClient } from '@dolittle/sdk';
import { TenantId } from '@dolittle/sdk.execution';
import { setTimeout } from 'timers/promises';

import { Employee } from './Employee';

(async () => {
    const client = await DolittleClient
        .setup()
        .connect();

    await setTimeout(2000);

    const updatedEmployee = new Employee(
        'Mr. Taco',
        'Street Food Taco Truck');

    await client.embeddings
        .forTenant(TenantId.development)
        .update(Employee, updatedEmployee.name, updatedEmployee);
    client.logger.info(`Updated ${updatedEmployee.name}`);

    await client.embeddings
        .forTenant(TenantId.development)
        .delete(Employee, updatedEmployee.name);
    client.logger.info(`Deleted ${updatedEmployee.name}`);
})();

The update() method tries to update the embeddings read model with the specified key to match the updated state by calling the embeddings @resolveUpdateToEvents() decorated method. If no read model exists with the key, it will create one with the read model set to the embedding’s initial state.

The delete() method will call the embeddings @resolveDeletionToEvents() decorated method for the specified key. This method then returns one or many events, which when handled will delete the read model.

Run your microservice

Let’s run the code! It should commit events to the event log, one for hiring "Mr. Taco", one for transferring him to "Street Food Taco Truck", and one for Mr. Tacos retirement.

$ dotnet run
info: Dolittle.SDK.DolittleClientService[0]
      Connecting Dolittle Client
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: .../Embeddings
info: Dolittle.SDK.Events.Processing.EventProcessors[0]
      Embedding e5577d2c-0de7-481c-b5be-6ef613c2fcd6 registered with the Runtime, start handling requests
Updated Mr. Taco.
Deleted Mr. Taco.

$ npx ts-node index.ts
info: Embedding e5577d2c-0de7-481c-b5be-6ef613c2fcd6 registered with the Runtime, start handling requests.
info: Updated Mr. Taco
info: Deleted Mr. Taco

Check the events

Let’s check the committed events from the embedding. Since the embedding commits aggregate events, we can use the Dolittle CLI to list the events with the following commands:

$ dolittle runtime aggregates get e5577d2c-0de7-481c-b5be-6ef613c2fcd6 --wide
Tenant                                EventSource  AggregateRootVersion
-----------------------------------------------------------------------
445f8ea8-1a6f-40d7-b2fc-796dba92dc44  Mr. Taco     3

$ dolittle runtime aggregates events e5577d2c-0de7-481c-b5be-6ef613c2fcd6 "Mr. Taco"
AggregateRootVersion  EventLogSequenceNumber  EventType          
-----------------------------------------------------------------
0                     0                       EmployeeHired      
1                     1                       EmployeeTransferred
2                     2                       EmployeeRetired

The aggregate root id argument is the same as the embedding id.

Get the Embeddings

You can also get the read models and keys for an embedding. This can be useful when figuring out what states still exist in the external system compared to the embeddings read models or when debugging.

For example, the HR system might only return the currently hired employees. Any employees not returned by the HR system but still in the embedding could then be marked as retired.

using System;
using System.Threading.Tasks;
using Dolittle.SDK;
using Dolittle.SDK.Tenancy;
using Microsoft.Extensions.Hosting;

var host = Host.CreateDefaultBuilder()
    .UseDolittle()
    .Build();

await host.StartAsync();

var client = await host.GetDolittleClient();

var updatedEmployee = new Employee
{
    Name = "Mr. Taco",
    Workplace = "Street Food Taco Truck"
};

await Task.Delay(TimeSpan.FromSeconds(4));

await client.Embeddings
    .ForTenant(TenantId.Development)
    .Update(updatedEmployee.Name, updatedEmployee);
Console.WriteLine($"Updated {updatedEmployee.Name}.");
var mrTaco = await client.Embeddings
    .ForTenant(TenantId.Development)
    .Get<Employee>("Mr. Taco");
Console.WriteLine($"Mr. Taco is now working at {mrTaco.State.Workplace}");

var allEmployeeNames = await client.Embeddings
    .ForTenant(TenantId.Development)
    .GetKeys<Employee>();
Console.WriteLine($"All current employees are {string.Join(",", allEmployeeNames)}");

await client.Embeddings
    .ForTenant(TenantId.Development)
    .Delete<Employee>(updatedEmployee.Name);
Console.WriteLine($"Deleted {updatedEmployee.Name}.");

await host.WaitForShutdownAsync();

import { DolittleClient } from '@dolittle/sdk';
import { TenantId } from '@dolittle/sdk.execution';
import { setTimeout } from 'timers/promises';

import { Employee } from './Employee';

(async () => {
    const client = await DolittleClient
        .setup()
        .connect();

    await setTimeout(2000);

    const updatedEmployee = new Employee(
        'Mr. Taco',
        'Street Food Taco Truck');

    await client.embeddings
        .forTenant(TenantId.development)
        .update(Employee, updatedEmployee.name, updatedEmployee);
    client.logger.info(`Updated ${updatedEmployee.name}`);

    const mrTaco = await client.embeddings
        .forTenant(TenantId.development)
        .get(Employee, 'Mr. Taco');
    client.logger.info(`Mr. Taco is now working at ${mrTaco.state.workplace}`);

    const allEmployeeNames = await client.embeddings
        .forTenant(TenantId.development)
        .getKeys(Employee);
    client.logger.info(`All current employees are ${allEmployeeNames}`);

    await client.embeddings
        .forTenant(TenantId.development)
        .delete(Employee, updatedEmployee.name);
    client.logger.info(`Deleted ${updatedEmployee.name}`);
})();

Running the code with the modifications above, should print the following:

$ dotnet run
info: Dolittle.SDK.DolittleClientService[0]
      Connecting Dolittle Client
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: .../Embeddings
info: Dolittle.SDK.Events.Processing.EventProcessors[0]
      Embedding e5577d2c-0de7-481c-b5be-6ef613c2fcd6 registered with the Runtime, start handling requests
Updated Mr. Taco.
Mr. Taco is now working at Street Food Taco Truck
All current employees are Mr. Taco
Deleted Mr. Taco.

$ npx ts-node index.ts
info: Embedding e5577d2c-0de7-481c-b5be-6ef613c2fcd6 registered with the Runtime, start handling requests.
info: Updated Mr. Taco
info: Mr. Taco is now working at Street Food Taco Truck
info: All current employees are Mr. Taco
info: Deleted Mr. Taco

What’s next

5 - Event Horizon

Get started with the Event Horizon

Welcome to the tutorial for Event Horizon, where you learn how to write a Microservice that produces public events of dishes prepared by chefs, and another microservice consumes those events.

After this tutorial you will have:

Use the tabs to switch between the C# and TypeScript code examples. Full tutorial code available on GitHub for C# and TypeScript.

Prerequisites

This tutorial builds directly upon the getting started guide and the files from the it.

Setup

This tutorial will have a setup with two microservices; one that produces public events, and a consumer that subscribes to those public events. Let’s make a folder structure that resembles that:

└── event-horizon-tutorial/
    ├── consumer/
    ├── producer/
    └── environment/
        └── docker-compose.yml

Go into both the consumer and the producer folders and initiate the project as we’ve gone through in our getting started guide. I.e copy over all the code from the getting started tutorial to the consumer and producer folders. You can choose different languages for the microservices if you want to.

We’ll come back to the docker-compose later in this tutorial.

Producer

Create a Public Filter

A public filter filters all public events that pass the filter into a public stream, which is special stream that another microservice can subscribe to.

A public filter is defined as a method that returns a partitioned filter result, which is an object with two properties:

  • a boolean that says whether the event should be included in the public stream
  • a partition id which is the partition that the event should belong to in the public stream.

Only public events get filtered through the public filters.

// Program.cs
using System;
using System.Threading.Tasks;
using Dolittle.SDK;
using Dolittle.SDK.Events.Filters;
using Dolittle.SDK.Tenancy;
using Microsoft.Extensions.Hosting;

var host = Host.CreateDefaultBuilder()
    .UseDolittle(_ => _
        .WithFilters(_ => _
            .CreatePublic("2c087657-b318-40b1-ae92-a400de44e507")
            .Handle((@event, eventContext) =>
            {
                Console.WriteLine($"Filtering event {@event} to public streams");
                return Task.FromResult(new PartitionedFilterResult(true, eventContext.EventSourceId.Value));
            })))
    .Build();

await host.StartAsync();
await host.WaitForShutdownAsync();

// index.ts
import { DolittleClient } from '@dolittle/sdk';
import { EventContext } from '@dolittle/sdk.events';
import { PartitionedFilterResult } from '@dolittle/sdk.events.filtering';
import { TenantId } from '@dolittle/sdk.execution';

import './DishHandler';
import { DishPrepared } from './DishPrepared';

(async () => {
    const client = await DolittleClient
        .setup(_ => _
            .withFilters(_ => _
                .createPublic('2c087657-b318-40b1-ae92-a400de44e507')
                    .handle((event: any, context: EventContext) => {
                        client.logger.info(`Filtering event ${JSON.stringify(event)} to public stream`);
                        return new PartitionedFilterResult(true, 'Dolittle Tacos');
                    })
            )
        )
        .connect();
})();

Notice that the returned PartitionedFilterResult has true and an unspecified PartitionId (which is the same as an empty GUID). This means that this filter creates a public stream that includes all public events, and that they are put into the unspecified partition of that stream.

Commit the public event

Now that we have a public stream we can commit public events to start filtering them. Let’s commit a DishPrepared event as a public event from the producer microservice:

// Program.cs
using System;
using System.Threading.Tasks;
using Dolittle.SDK;
using Dolittle.SDK.Events.Filters;
using Dolittle.SDK.Tenancy;
using Microsoft.Extensions.Hosting;

var host = Host.CreateDefaultBuilder()
    .UseDolittle(_ => _
        .WithFilters(_ => _
            .CreatePublic("2c087657-b318-40b1-ae92-a400de44e507")
            .Handle((@event, eventContext) =>
            {
                Console.WriteLine($"Filtering event {@event} to public streams");
                return Task.FromResult(new PartitionedFilterResult(true, eventContext.EventSourceId.Value));
            })))
    .Build();

await host.StartAsync();

var client = await host.GetDolittleClient();
var preparedTaco = new DishPrepared("Bean Blaster Taco", "Mr. Taco");
await client.EventStore
    .ForTenant(TenantId.Development)
    .CommitPublicEvent(preparedTaco, "Dolittle Tacos");

await host.WaitForShutdownAsync();

// index.ts
import { DolittleClient } from '@dolittle/sdk';
import { EventContext } from '@dolittle/sdk.events';
import { PartitionedFilterResult } from '@dolittle/sdk.events.filtering';
import { TenantId } from '@dolittle/sdk.execution';

import './DishHandler';
import { DishPrepared } from './DishPrepared';

(async () => {
    const client = await DolittleClient
        .setup(_ => _
            .withFilters(_ => _
                .createPublic('2c087657-b318-40b1-ae92-a400de44e507')
                    .handle((event: any, context: EventContext) => {
                        client.logger.info(`Filtering event ${JSON.stringify(event)} to public stream`);
                        return new PartitionedFilterResult(true, 'Dolittle Tacos');
                    })
            )
        )
        .connect();

    const preparedTaco = new DishPrepared('Bean Blaster Taco', 'Mr. Taco');

    await client.eventStore
        .forTenant(TenantId.development)
        .commitPublic(preparedTaco, 'Dolittle Tacos');
})();

Now we have a producer microservice with a public stream of DishPrepared events.

Consumer

Subscribe to the public stream of events

Let’s create another microservice that subscribes to the producer’s public stream.

// Program.cs
using System;
using Dolittle.SDK;
using Dolittle.SDK.Tenancy;
using Microsoft.Extensions.Hosting;

Host.CreateDefaultBuilder()
    .UseDolittle(_ => _
        .WithEventHorizons(_ => _
            .ForTenant(TenantId.Development, subscriptions => 
                subscriptions
                    .FromProducerMicroservice("f39b1f61-d360-4675-b859-53c05c87c0e6")
                    .FromProducerTenant(TenantId.Development)
                    .FromProducerStream("2c087657-b318-40b1-ae92-a400de44e507")
                    .FromProducerPartition("Dolittle Tacos")
                    .ToScope("808ddde4-c937-4f5c-9dc2-140580f6919e"))
        )
        .WithEventHandlers(_ => _
            .Create("6c3d358f-3ecc-4c92-a91e-5fc34cacf27e")
            .InScope("808ddde4-c937-4f5c-9dc2-140580f6919e")
            .Partitioned()
            .Handle<DishPrepared>((@event, context) => Console.WriteLine($"Handled event {@event} from public stream"))
        ),
        configuration => configuration.WithRuntimeOn("localhost", 50055))
    .Build()
    .Run();

// index.ts
import { DolittleClient } from '@dolittle/sdk';
import { TenantId } from '@dolittle/sdk.execution';

import { DishPrepared } from './DishPrepared';

(async () => {
    const client = await DolittleClient
        .setup(_ => _
            .withEventHorizons(_ => {
                _.forTenant(TenantId.development, _ => _
                    .fromProducerMicroservice('f39b1f61-d360-4675-b859-53c05c87c0e6')
                        .fromProducerTenant(TenantId.development)
                        .fromProducerStream('2c087657-b318-40b1-ae92-a400de44e507')
                        .fromProducerPartition('Dolittle Tacos')
                        .toScope('808ddde4-c937-4f5c-9dc2-140580f6919e'));
            })
            .withEventHandlers(_ => _
                .create('6c3d358f-3ecc-4c92-a91e-5fc34cacf27e')
                    .inScope('808ddde4-c937-4f5c-9dc2-140580f6919e')
                    .partitioned()
                    .handle(DishPrepared, (event, context) => {
                        client.logger.info(`Handled event ${JSON.stringify(event)} from public stream`);
                     })
            )
        )
        .connect(_ => _
            .withRuntimeOn('localhost', 50055)
        );
})();

Now we have a consumer microservice that:

  • Connects to another Runtime running on port 50055
  • Subscribes to the producer’s public stream with the id of 2c087657-b318-40b1-ae92-a400de44e507 (same as the producer’s public filter)
  • Puts those events into a Scope with id of 808ddde4-c937-4f5c-9dc2-140580f6919e
  • Handles them incoming events in a scoped event handler with an id of 6c3d358f-3ecc-4c92-a91e-5fc34cacf27e

There’s a lot of stuff going on the code so let’s break it down:

Connection to the Runtime

configuration => configuration.WithRuntimeOn("localhost", 50055))

.withRuntimeOn('localhost', 50055)

This line configures the hostname and port of the Runtime for this client. By default, it connects to the Runtimes default port of 50053 on localhost.

Since we in this tutorial will end up with two running instances of the Runtime, they will have to run with different ports. The producer Runtime will be running on the default 50053 port, and the consumer Runtime will be running on port 50055. We’ll see this reflected in the docker-compose.yml file later in this tutorial.

Event Horizon

// Program.cs
.WithEventHorizons(_ => _
    .ForTenant(TenantId.Development, subscriptions => 
        subscriptions
            .FromProducerMicroservice("f39b1f61-d360-4675-b859-53c05c87c0e6")
            .FromProducerTenant(TenantId.Development)
            .FromProducerStream("2c087657-b318-40b1-ae92-a400de44e507")
            .FromProducerPartition("Dolittle Tacos")
            .ToScope("808ddde4-c937-4f5c-9dc2-140580f6919e"))
)

.withEventHorizons(_ => {
    _.forTenant(TenantId.development, _ => _
        .fromProducerMicroservice('f39b1f61-d360-4675-b859-53c05c87c0e6')
            .fromProducerTenant(TenantId.development)
            .fromProducerStream('2c087657-b318-40b1-ae92-a400de44e507')
            .fromProducerPartition('Dolittle Tacos')
            .toScope('808ddde4-c937-4f5c-9dc2-140580f6919e'));
})

Here we define an event horizon subscription. Each subscription is submitted and managed by the Runtime. A subscription defines:

When the consumer’s Runtime receives a subscription, it will send a subscription request to the producer’s Runtime. If the producer accepts that request, the producer’s Runtime will start sending the public stream over to the consumer’s Runtime, one event at a time.

The acceptance depends on two things:

  • The consumer needs to know where to access the other microservices, ie the URL address.
  • The producer needs to give formal Consent for a tenant in another microservice to subscribe to public streams of a tenant.

We’ll setup the consent later.

The consumer will receive events from the producer and put those events in a specialized event-log that is identified by the scope’s id, so that events received over the event horizon don’t mix with private events. We’ll talk more about the scope when we talk about the scoped event handler.

Scoped Event Handler

// Program.cs
.WithEventHandlers(_ => _
    .Create("6c3d358f-3ecc-4c92-a91e-5fc34cacf27e")
    .InScope("808ddde4-c937-4f5c-9dc2-140580f6919e")
    .Partitioned()
    .Handle<DishPrepared>((@event, context) => Console.WriteLine($"Handled event {@event} from public stream"))
)

.withEventHandlers(_ => _
    .create('6c3d358f-3ecc-4c92-a91e-5fc34cacf27e')
        .inScope('808ddde4-c937-4f5c-9dc2-140580f6919e')
        .partitioned()
        .handle(DishPrepared, (event, context) => {
            client.logger.info(`Handled event ${JSON.stringify(event)} from public stream`);
        })
)

Here we use the opportunity to create an event handler inline by using the client’s builder function. This way we don’t need to create a class and register it as an event handler.

This code will create a partitioned event handler with id 6c3d358f-3ecc-4c92-a91e-5fc34cacf27e (same as from getting started) in a specific scope.

Remember, that the events from an event horizon subscription get put into a scoped event-log that is identified by the scope id. Having the scope id defined when creating an event handler signifies that it will only handle events in that scope and no other.

Setup your environment

Now we have the producer and consumer microservices Heads coded, we need to setup the environment for them to run in and configure their Runtimes to be connected. This configuration is provided by Dolittle when you’re running your microservices in our platform, but when running multiple services on your local machine you need to configure some of it yourself.

Let’s go to the environment folder we created in the beginning of this tutorial. Here we’ll need to configure:

Platform

platform.json configures the environment of a microservice. We have 2 microservices so they need to be configured with different identifiers and names.

Let’s create 2 files, consumer-platform.json and producer-producer.json:

//consumer-platform.json
{
    "applicationName": "EventHorizon Tutorial",
    "applicationID": "5bd8762f-6c39-4ba2-a141-d041c8668894",
    "microserviceName": "Consumer",
    "microserviceID": "a14bb24e-51f3-4d83-9eba-44c4cffe6bb9",
    "customerName": "Dolittle Tacos",
    "customerID": "c2d49e3e-9bd4-4e54-9e13-3ea4e04d8230",
    "environment": "Tutorial"
}
//producer-platform.json
{
    "applicationName": "EventHorizon Tutorial",
    "applicationID": "5bd8762f-6c39-4ba2-a141-d041c8668894",
    "microserviceName": "Producer",
    "microserviceID": "f39b1f61-d360-4675-b859-53c05c87c0e6",
    "customerName": "Dolittle Tacos",
    "customerID": "c2d49e3e-9bd4-4e54-9e13-3ea4e04d8230",
    "environment": "Tutorial"
}

Resources

resources.json configures where a microservices stores its event store. We have 2 microservices so they both need their own event store database. By default the database is called event_store.

Create 2 more files, consumer-resources.json and producer-resources.json:

//consumer-resources.json
{
    // the tenant to define this resource for
    "445f8ea8-1a6f-40d7-b2fc-796dba92dc44": {
        "eventStore": {
            "servers": [
                // hostname of the mongodb
                "mongo"
            ],
            // the database name for the event store
            "database": "consumer_event_store"
        }
    }
}
//producer-resources.json
{
    // the tenant to define this resource for
    "445f8ea8-1a6f-40d7-b2fc-796dba92dc44": {
        "eventStore": {
            "servers": [
                // hostname of the mongodb
                "mongo"
            ],
            // the database name for the event store
            "database": "producer_event_store"
        }
    }
}

Microservices

microservices.json configures where the producer microservice is so that the consumer can connect to it and subscribe to its events.

Let’s create a consumer-microservices.json file to define where the consumer can find the producer:

// consumer-microservices.json
{
    // the producer microservices id, hostname and port
    "f39b1f61-d360-4675-b859-53c05c87c0e6": {
        "host": "producer-runtime",
        "port": 50052
    }
}

event-horizon-consents.json configures the Consents that the producer gives to consumers.

Let’s create producer-event-horizon-consents.json where we give a consumer consent to subscribe to our public stream.

// producer-event-horizon-consents.json
{
    // the producer's tenant that gives the consent
    "445f8ea8-1a6f-40d7-b2fc-796dba92dc44": [
        {
            // the consumer's microservice and tenant to give consent to
            "microservice": "a14bb24e-51f3-4d83-9eba-44c4cffe6bb9",
            "tenant": "445f8ea8-1a6f-40d7-b2fc-796dba92dc44",
            // the producer's public stream and partition to give consent to subscribe to
            "stream": "2c087657-b318-40b1-ae92-a400de44e507",
            "partition": "Dolittle Tacos",
            // an identifier for this consent. This is random
            "consent": "ad57aa2b-e641-4251-b800-dd171e175d1f"
        }
    ]
}

Configure docker-compose.yml

Now we can glue all the configuration files together in the docker-compose.yml. The configuration files are mounted inside /app.dolittle/ inside the dolittle/runtime image.

version: '3.8'
services:
  mongo:
    image: dolittle/mongodb
    hostname: mongo
    ports:
      - 27017:27017
    logging:
      driver: none
 
  consumer-runtime:
    image: dolittle/runtime:latest
    volumes:
      - ./consumer-platform.json:/app/.dolittle/platform.json
      - ./consumer-resources.json:/app/.dolittle/resources.json
      - ./consumer-microservices.json:/app/.dolittle/microservices.json
    ports:
      - 50054:50052
      - 50055:50053

  producer-runtime:
    image: dolittle/runtime:latest
    volumes:
      - ./producer-platform.json:/app/.dolittle/platform.json
      - ./producer-resources.json:/app/.dolittle/resources.json
      - ./producer-event-horizon-consents.json:/app/.dolittle/event-horizon-consents.json
    ports:
      - 50052:50052
      - 50053:50053

Start the environment

Start the docker-compose with this command

$ docker-compose up -d

This will spin up a MongoDB container and two Runtimes in the background.

Run your microservices

Run both the consumer and producer microservices in their respective folders, and see the consumer handle the events from the producer:

Producer

$ dotnet run
info: Dolittle.SDK.DolittleClientService[0]
      Connecting Dolittle Client
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/jakob/Git/dolittle/DotNET.SDK/Samples/Tutorials/EventHorizon/Producer
info: Dolittle.SDK.Events.Processing.EventProcessors[0]
      Public Filter 2c087657-b318-40b1-ae92-a400de44e507 registered with the Runtime, start handling requests
info: Dolittle.SDK.Events.Processing.EventProcessors[0]
      EventHandler f2d366cf-c00a-4479-acc4-851e04b6fbba registered with the Runtime, start handling requests
Filtering event DishPrepared to public streams
info: DishHandler[0]
      Mr. Taco has prepared Bean Blaster Taco. Yummm!

Consumer

$ dotnet run
info: Dolittle.SDK.DolittleClientService[0]
      Connecting Dolittle Client
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/jakob/Git/dolittle/DotNET.SDK/Samples/Tutorials/EventHorizon/Consumer
info: Dolittle.SDK.Events.Processing.EventProcessors[0]
      EventHandler 6c3d358f-3ecc-4c92-a91e-5fc34cacf27e registered with the Runtime, start handling requests
Handled event DishPrepared from public stream

Producer

$ npx ts-node index.ts
info: EventHandler f2d366cf-c00a-4479-acc4-851e04b6fbba registered with the Runtime, start handling requests.
info: Public Filter 2c087657-b318-40b1-ae92-a400de44e507 registered with the Runtime, start handling requests.
info: Filtering event {"Dish":"Bean Blaster Taco","Chef":"Mr. Taco"} to public stream
info: Mr. Taco has prepared Bean Blaster Taco. Yummm!

Consumer

$ npx ts-node index.ts
info: EventHandler 6c3d358f-3ecc-4c92-a91e-5fc34cacf27e registered with the Runtime, start handling requests.
info: Handled event {"Dish":"Bean Blaster Taco","Chef":"Mr. Taco"} from public stream

What’s next