This is the multi-page printable view of this section. Click here to print.
Tutorials
- 1: Getting started
- 2: Aggregates
- 3: Projections
- 4: Event Horizon
1 - Getting started
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, and
- a Microservice that commits and handles Events
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(
content: new DishPrepared("Bean Blaster Taco", "Mr. Taco"),
eventSourceId: "Dolittle Tacos");
await host.WaitForShutdownAsync();
The string given as eventSourceId
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.
Docker on Windows
Docker on Windows using the WSL2 backend can use massive amounts of RAM if not limited. Configuring a limit in the.wslconfig
file can help greatly, as mentioned in this issue. The RAM usage is also lowered if you disable the WSL2 backend in Docker for Desktop settings.
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
- Learn how to use Aggregates implement rules.
- Learn how to use Projections to create read models.
- Learn how to deploy your application into our Platform.
2 - 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.
Docker on Windows
Docker on Windows using the WSL2 backend can use massive amounts of RAM if not limited. Configuring a limit in the.wslconfig
file can help greatly, as mentioned in this issue. The RAM usage is also lowered if you disable the WSL2 backend in Docker for Desktop settings.
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 ✅
Committing events outside of an aggregate root
If you went through the getting started tutorial and this tutorial without stopping the Dolittle environment in between, the last command will show that the DishHandler has handled 3 events - even though the Kitchen can only prepare two dishes. This is fine, and expected behavior. Events committed outside of the Kitchen aggregate root (even if they are the same type), does not update the internal state.What’s next
- Learn how to use Projections to create read models.
- Learn how to deploy your application into our Platform.
3 - 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:
- a running Dolittle environment with a Runtime and a MongoDB, and
- a Microservice that commits Events
- 2 Projections that react to events and mutate the Read Model.
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.
Docker on Windows
Docker on Windows using the WSL2 backend can use massive amounts of RAM if not limited. Configuring a limit in the.wslconfig
file can help greatly, as mentioned in this issue. The RAM usage is also lowered if you disable the WSL2 backend in Docker for Desktop settings.
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.
Docker on Windows
Docker on Windows using the WSL2 backend can use massive amounts of RAM if not limited. Configuring a limit in the.wslconfig
file can help greatly, as mentioned in this issue. The RAM usage is also lowered if you disable the WSL2 backend in Docker for Desktop settings.
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
- Learn how to deploy your application into our Platform.
4 - 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:
- a running Dolittle environment with two Runtimes and a MongoDB,
- a Producer Microservice that commits and handles a Public Event and filters it into a Public Stream and
- a Consumer Microservice that Subscribes to the consumers public stream over the Event Horizon and processes those public events
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.
Prerequisites:
Prerequisites:
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:
- The consumers Tenant
- The producer microservice, Public Stream and that streams Partition to get the events from
- The Scoped event-log of the consumer to put the subscribed events to
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"
}
}
}
Development Tenant
The tenant id445f8ea8-1a6f-40d7-b2fc-796dba92dc44
is the value of TenantId.Development
.
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
}
}
Consent
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
Resource file naming
The configuration files mounted inside the image need to be named as they are defined in the configuration reference. Otherwise the Runtime can’t find them.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.
Docker on Windows
Docker on Windows using the WSL2 backend can use massive amounts of RAM if not limited. Configuring a limit in the.wslconfig
file can help greatly, as mentioned in this issue. The RAM usage is also lowered if you disable the WSL2 backend in Docker for Desktop settings.
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
- Learn how to deploy your application into our Platform
- Learn more about the Event Horizon