This 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

This tutorial makes use of experimental decorators. To enable it simply make sure you have “experimentalDecorators” set to true in your tsconfig.json.

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;

namespace Kitchen 
{
    [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 System;
using Dolittle.SDK.Events;
using Dolittle.SDK.Events.Handling;

namespace Kitchen
{
    [EventHandler("f2d366cf-c00a-4479-acc4-851e04b6fbba")]
    public class DishHandler
    {
        public void Handle(DishPrepared @event, EventContext eventContext)
        {
            Console.WriteLine($"{@event.Chef} has prepared {@event.Dish}. Yummm!");
        }
    }
}

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 { EventContext } from '@dolittle/sdk.events';
import { eventHandler, handles } from '@dolittle/sdk.events.handling';
import { DishPrepared } from './DishPrepared';

@eventHandler('f2d366cf-c00a-4479-acc4-851e04b6fbba')
export class DishHandler {

    @handles(DishPrepared)
    dishPrepared(event: DishPrepared, eventContext: EventContext) {
        console.log(`${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;

namespace Kitchen
{
    class Program
    {
        public static void Main()
        {
            var client = Client
                .ForMicroservice("f39b1f61-d360-4675-b859-53c05c87c0e6")
                .WithEventTypes(eventTypes =>
                    eventTypes.Register<DishPrepared>())
                .WithEventHandlers(builder =>
                    builder.RegisterEventHandler<DishHandler>())
                .Build();

            var preparedTaco = new DishPrepared("Bean Blaster Taco", "Mr. Taco");

            client.EventStore
                .ForTenant(TenantId.Development)
                .Commit(eventsBuilder =>
                    eventsBuilder
                        .CreateEvent(preparedTaco)
                        .FromEventSource("bfe6f6e4-ada2-4344-8a3b-65a3e1fe16e9"));

            // Blocks until the EventHandlers are finished, i.e. forever
            client.Start().Wait();
        }
    }
}

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

// index.ts
import { Client } from '@dolittle/sdk';
import { TenantId } from '@dolittle/sdk.execution';
import { DishPrepared } from './DishPrepared';
import { DishHandler } from './DishHandler';

const client = Client
    .forMicroservice('f39b1f61-d360-4675-b859-53c05c87c0e6')
    .withEventTypes(eventTypes =>
        eventTypes.register(DishPrepared))
    .withEventHandlers(builder =>
        builder.register(DishHandler))
    .build();

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

client.eventStore
    .forTenant(TenantId.development)
    .commit(preparedTaco, 'bfe6f6e4-ada2-4344-8a3b-65a3e1fe16e9');

The GUID 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 27017:27017 dolittle/runtime:latest-development

This will start a container with the Dolittle Development Runtime on port 50053 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
Mr. Taco has prepared Bean Blaster Taco. Yummm!

$ npx ts-node index.ts
Mr. Taco has prepared Bean Blaster Taco. Yummm!

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) for the aggregates of that aggregate root. It encapsulates the domain objects, enforces business rules, and ensures that the aggregate can’t be put into an invalid state. The aggregate root usually exposes methods that creates and applies an event.

There are essentially two types of aggregate roots, stateless and stateful. The aggregate root in this example is stateful because it tracks a value called _counter that is used to control the invariant that no more than two dishes can be prepared. Stateful aggregate roots have On() methods that takes in a single parameter, an event type. Each time an event of that type is applied to this aggregate root the On method will be called. It is important that the On methods only updates the internal state of the aggregate root!

// Kitchen.cs

using System;
using Dolittle.SDK.Aggregates;
using Dolittle.SDK.Events;

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

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

        public void PrepareDish(string dish, string chef)
        {
            if (_counter >= 2) throw new Exception("Cannot prepare more than 2 dishes");
            Apply(new DishPrepared(dish, chef));
            Console.WriteLine($"Kitchen Aggregate {EventSourceId} has applied {_counter} {typeof(DishPrepared)} events");
        }

        void On(DishPrepared @event)
            => _counter++;
    }
}

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 _counter: number = 0;

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

    prepareDish(dish: string, chef: string) {
        if (this._counter >= 2) throw new Error("Cannot prepare more than 2 dishes");
        this.apply(new DishPrepared(dish, chef));
        console.log(`Kitchen Aggregate ${this.eventSourceId} has applied ${this._counter} ${DishPrepared.name} events`);
    }


    @on(DishPrepared)
    onDishPrepared(event: DishPrepared) {
        this._counter++;
    }
}

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;

namespace Kitchen
{
    class Program
    {
        public static void Main()
        {
            var client = Client
                .ForMicroservice("f39b1f61-d360-4675-b859-53c05c87c0e6")
                .WithEventTypes(eventTypes =>
                    eventTypes.Register<DishPrepared>())
                .WithEventHandlers(builder =>
                    builder.RegisterEventHandler<DishHandler>())
                .Build();

            client
                .AggregateOf<Kitchen>("bfe6f6e4-ada2-4344-8a3b-65a3e1fe16e9", _ => _.ForTenant(TenantId.Development))
                .Perform(kitchen => kitchen.PrepareDish("Bean Blaster Taco", "Mr. Taco"));

            // Blocks until the EventHandlers are finished, i.e. forever
            client.Start().Wait();
        }
    }
}

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

// index.ts
import { Client } from '@dolittle/sdk';
import { TenantId } from '@dolittle/sdk.execution';
import { DishPrepared } from './DishPrepared';
import { DishHandler } from './DishHandler';
import { Kitchen } from './Kitchen';

(async () => {
    const client = Client
        .forMicroservice('f39b1f61-d360-4675-b859-53c05c87c0e6')
        .withEventTypes(eventTypes =>
            eventTypes.register(DishPrepared))
        .withEventHandlers(builder =>
            builder.register(DishHandler))
        .build();

    await client
        .aggregateOf(Kitchen, 'bfe6f6e4-ada2-4344-8a3b-65a3e1fe16e9', _ => _.forTenant(TenantId.development))
        .perform(kitchen => kitchen.prepareDish('Bean Blaster Taco', 'Mr. Taco'));

    console.log('Done');
})();

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

Start the Dolittle environment

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

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

This will start a container with the Dolittle Development Runtime on port 50053 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
Mr. Taco has prepared Bean Blaster Taco. Yummm!

$ npx ts-node index.ts
Mr. Taco has prepared Bean Blaster Taco. Yummm!

What’s next