Tutorial: Hello World with Microsoft Orleans

This topic is a bit of a divergence from regular content regarding building mobile, desktop and web apps using cross platform technologies like Uno and Flutter. However, you shouldn’t underestimate the complexity of building scalable backend services to support mobile applications. Whilst this post will be an introductory post on gettting started with Microsoft Orleans, in future posts I’ll move on to look at how it can be used to support an Event Sourcing architecture for your backend services.

Update 8/3/2022: Code Repository

Before we get started, if you haven’t heard of Microsoft Orleans, or you haven’t worked with it before, I would strongly encourage you to read the overview of Microsoft Orleans which will give you the background on what and why Orleans is relevant when building scalable backend services.

In this post we’re going to build a Web API service (the frontend) which connects to a Microsoft Orleans Silo (the backend), and can be hosted on an Azure App Service and uses Azure Table Storage (or Azure Cosmos DB) for storage. The simplicity of this configuration makes it possible for developers to leverage Microsoft Orleans, without having to onboard any of the complexity of using containers and kubernetes.

This sample will consist of four projects:

  • HelloWorld.Services – The Web API project that exposes a public endpoint that can be called by an application (references HelloWorld.Abstractions)
  • HelloWorld.Silo – The Microsoft Orleans Silo that will create and hold the grain instances (references both HelloWorld.Abstractions and HelloWorld.Entities)
  • HelloWorld.Abstractions – The grain interface definitions
  • HelloWorld.Entities – The grain implementations (references HelloWorld.Abstractions).

We’ll get started with creating the Services and Silo projects. Create the HelloWorld.Services based on the ASP.NET Core Web Api project template in Visual Studio.

Name the project and click Next

For the purpose of this post we’ll uncheck the use of controllers (ie take the minimal api option), and click Create.

Repeat this process for the HelloWorld.Silo project.

Next, we need to add two class libraries, HelloWorld.Abstractions and HelloWorld.Entities

Now that we’ve created the projects we need to setup some references between them. Add references between the following projects:

  • HelloWorld.Entities references HelloWorld.Abstractions
  • HelloWorld.Services and HelloWorld.Silo both reference HelloWorld.Abstractions
  • HelloWorld.Silo references HelloWorld.Entities

Note: In the past you’d have to right-click each project and select Add -> Project Reference in order to add dependencies. With Visual Studio 2022 you can simply drag the project node you want to add a dependency to onto the project node that you want to add the dependency to (eg drag the HelloWorld.Abstractions project node onto the HelloWorld.Entities project node in the Solution Explorer tool window).

Once we’re done establishing the references between projects, the solution should look similar to the following image.

The next step is to add references to Microsoft.Orleans.Core.Abstractions and Microsoft.Orleans.CodeGenerator.MSBuild nuget packages. These packages should be added to all four projects.

The last piece of setup we need to do is to copy the Orleans.Azure.Infrastructure project, produced by Brady Gaster – this provides a bunch of silo and client builders for setting up both the silo and client service based on the configuration values specified. Once this project has been added to the solution, add references to the infrastructure project to both HelloWorld.Services and HelloWorld.Silo projects.

Now that we’ve setup all the projects, let’s start by creating the interface for our HelloWorld grain. In the HelloWorld.Abstractions project, change Class1.cs to IHelloWorld.cs and add the following IHelloWorld interface.

using Orleans;

namespace HelloWorld.Abstractions;

public interface IHelloWorld : IGrainWithGuidKey
{
    Task<string> SayHelloWorld();
}

In the HelloWorld.Entities project change Class1.cs to HelloWorld.cs and add the following HelloWorld class, which implements the IHelloWorld interface and inherits from the Grain class.

using HelloWorld.Abstractions;
using Orleans;

namespace HelloWorld.Entities;

public class HelloWorld : Grain, IHelloWorld
{
    public Task<string> SayHelloWorld()
    {
        return Task.FromResult("Hello World");
    }
}

As you can see from the HelloWolrd code, it’s surprisingly simple and just includes the implementation logic for the HelloWorld grain.

In order to make our HelloWorld grain accessible, we need to modify the HelloWorld.Silo project to use Microsoft Orleans. Replace the default code in program.cs with the following. During development, you might want to uncomment the “/” endpoint so that you can test that the silo is operational by navigating to the endpoint in a browser.

using HelloWorld.Abstractions;
using Orleans;
using Orleans.Hosting;

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseOrleans(siloBuilder =>
{
    var storageConnectionString = builder.Configuration.GetValue<string>(EnvironmentVariables.AzureStorageConnectionString);
    siloBuilder
        .HostSiloInAzure(builder.Configuration);
});

var app = builder.Build();
app.UseRouting();

// Uncomment this to expose an endpoint that can be opened
// in the browser to test that the silo is up and running
//app.MapGet("/", async (IGrainFactory grainFactory) =>
//{
//    var grain = grainFactory.GetGrain<IHelloWorld>(Guid.Empty);
//    return await grain.SayHelloWorld();
//});

app.Run();

With the HelloWorld.Silo operational, we now need to expose a frontend through which the silo can be accessed. Replace the default code in program.cs in the HelloWorld.Sevices project with the following.

using HelloWorld.Abstractions;
using Orleans;
using Orleans.Hosting;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddOrleansClusterClient(builder.Configuration);

var app = builder.Build();

app.UseRouting();

app.MapGet("/", async (IClusterClient orleansClient) =>
{
    var grain = orleansClient.GetGrain<IHelloWorld>(Guid.Empty);
    return await grain.SayHelloWorld();
});

app.Run();

Note: it’s easy to add the wrong code to the wrong program.cs file, which will result in errors stating that the HelloWorld grain is inaccessible. Make sure you add the code to the correct program.cs file!

In order for us to run the application, we need to make sure that both the silo and the frontend services are running. To do this, right-click on the solution node in Solution Explorer and select Set Startup Projects. Pick the Multiple startup projects optino and then change the dropdown to Start for both the HelloWorld.Silo and HelloWorld.Services project.

The last thing to do is to modify the launch settings for both the HelloWorld.Silo and HelloWorld.Services projects. In this case we’re going to remove the IIS settings and the IIS Express nodes, along with the weatherForecast launchurl (since this endpoint no longer exists).

When you run the solution by pressing F5 you should see two browsers (since the LaunchBrowser property is set to true on both projects). However, if you didn’t uncomment the endpoint on the HelloWorld.Silo project, you’ll see an error reported in one of the browser windows. Uncomment the endpoint and you should see that both browser windows display Hello World.

Since the HelloWorld.Services project has no direct reference to the HelloWorld.Entities project (and thus the HelloWorld class), the only way it can be returning the “Hello World” string is via the remote call to the HelloWorld.Silo project.

Our basic Hello World Orleans application is operational. In this case everything is running locally with all communications being done via localhost discovery. This won’t work for publishing to Azure, so in the next post we’ll look at using Azure Table storage, or Azure CosmosDB to faciliate service discovery.