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

Last modified January 20, 2022: Fix tutorial samples (e40d3f3)