Hello Orleans 4.0


With the advent of .NET 6, C# 10 and Orleans 4.0 (preview) it seems like a great time to take a fresh look at Microsoft Orleans (which is now included in the official Microsoft docs!).

What is Orleans?

Orleans is an open source framework for building distributed systems in .NET.

This makes it easier for a developer to build software which will scale from a single computer to running across a cluster of hundreds of machines cooperating to deliver a single service.

Orleans works well for situations where fast response times are important, by keeping data in memory, or for concurrent problems where Orleans’ turn-based concurrency model can make it easy to reason about mutating state and thread safety.

Getting Started

Let’s by building a very simple Orleans sample, just to get up and running.

The only prerequisite you’ll need is .NET 6.0.

https://dotnet.microsoft.com/download/dotnet/6.0

You can then create a new console application project using the dotnet command line tool.

> dotnet new console --name HelloWorld

This will create a basic “Hello World!” console application.

We can add a references to the Orleans Nuget packages we’ll need.

> dotnet add package Microsoft.Extensions.Hosting
> dotnet add package Microsoft.Orleans.CodeGenerator.MSBuild --version 4.0.0-preview1
> dotnet add package Microsoft.Orleans.Core --version 4.0.0-preview1
> dotnet add package Microsoft.Orleans.Core.Abstractions --version 4.0.0-preview1
> dotnet add package Microsoft.Orleans.Sdk --version 4.0.0-preview1
> dotnet add package Microsoft.Orleans.Server --version 4.0.0-preview1

A super-simple program that starts up an Orleans ‘Silo’ and calls a grain looks like this:

// Program.cs
using Orleans;
using Orleans.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;

// create the silo host
using var host = new HostBuilder()
  .UseOrleans(b => b.UseLocalhostClustering())
  .Build();

// start the silo
await host.StartAsync();

// get a reference to the grain and call it
await host.Services
  .GetRequiredService<IGrainFactory>()
  .GetGrain<IHelloGrain>("grain-key")
  .Hello("World");

// interface for the grain
interface IHelloGrain : IGrainWithStringKey
{
  Task Hello(string name);
}

// class for the grain
class HelloGrain : IHelloGrain
{
  public Task Hello(string name)
  {
    Console.WriteLine($"Hello {name}");
    return Task.CompletedTask;
  }
}

Breaking it down

There’s a lot going on in the above code, let’s break it down into the individual parts and explain the concepts.

Grain Interfaces

Let’s start with the IHelloWorld interface:

interface IHelloGrain : IGrainWithStringKey
{
  Task Hello(string name);
}

Orleans’ great strength is that it allows you to connect together lots of machines into a cluster allowing you to run your code across these machines as if they were one big computer.

Just as you would create objects in a normal C# program, Orleans will create objects for you, on demand, and will distribute them across the computers in your cluster. We call these objects ‘Grains’.

You can then start sending messages to these objects and Orleans will automatically create (or activate) them somewhere in the cluster when required.

The client doesn’t need to know if the grains exist in memory or not, from the perspective of the client they are always available.

You need an interface to define the messages that a grain can receive. We create methods on the interface to represent each message the grain can receive.

In this case our IHelloGrain has a single message (Hello) which a single argument (name). It doesn’t return any data, but these methods must be asynchronous and therefore return a Task. This interface inherits IGrainWithStringKey, which means that it uses a string as it’s identity, or primary key.

Grain Classes

We can implement the IHelloGrain interface with the HelloGrain class. With Orleans version 4 we can have POCO Grains. In previous versions it was necessary to inherit a Gain base class. You can still inherit from this class if you prefer.

class HelloGrain : IHelloGrain
{
  public Task Hello(string name)
  {
    Console.WriteLine($"Hello {name}");
    return Task.CompletedTask;
  }
}

Our method doesn’t do much, it’s intentionally simple, but in theory this is the place to add “business logic”. The code in here will always be executed in a turn-based concurrency model. This means that if we send multiple Hello messages to the same grain at the same time in parallel, the method will be executed in a serial way, taking turns, one after the other.

This greatly simplifies the programming model, removing the need for locks or thread-safe data structures, and reduces the chance of having race conditions and other unwanted emergent behaviors when the system is under load.

Building the Host

The Silo is the runtime that hosts grains. A silo is configured using the HostBuilder class, and then calling a .UseOrleans() extension method.

In previous version of Orleans you would start by creating a SiloHostBuilder, but this has now been retired in favour of the HostBuilder.

In this case we just create a local silo for development (using the .UseLocalHostClustering()). The following line starts the Silo.

using var host = new HostBuilder()
  .UseOrleans(b => b.UseLocalhostClustering())
  .Build();

await host.StartAsync();

Sending Messages to a Grain

To send a message to a grain, first you need to create reference to it using the client. The HostBuilder creates a client for us and registers it as a service, so we can use GetRequiredService on the host’s services to get the client.

The client can then be used to get a reference to the grain. The reference combines the interface for the grain (IHelloGrain) and the identity, which in this case can be any string, and is hard-coded to "grain-key".

We finish by sending a message to the grain by calling the Hello method:

await host.Services
  .GetRequiredService<IGrainFactory>()
  .GetGrain<IHelloGrain>("grain-key")
  .Hello("World");

Wrapping Up

It’s fun that we can create a ‘Hello World’ in a single file. Of course, this is not how we would build a production application, but it’s a great way of playing with Orleans, and getting a feel for how the framework works.

It’s interesting to compare this with the 2016 edition and seeing how both C# and Orleans have reduced boiler-plate and become slicker.