Getting Started with Microsoft Orleans 2.0 in .NET Core

Microsoft Orleans 2.0 was released less than two weeks ago. The biggest win here is .NET Core/Standard support, meaning that Orleans is cross-platform. In this article, we’ll see how to quickly get up and running with Orleans 2.0.

The configuration and hosting APIs have changed considerably, so the instructions here won’t work for earlier versions. See my old “Getting Started with Microsoft Orleans” article from November 2016 if you’re running Orleans 1.4. Orleans 1.5 is also different so you’ll need to check the documentation for that.

In order to keep this article practical and concise, it is necessary to limit its scope. We will not be covering what Orleans is or what it is used for. We will also not create a full project structure that is typical in Orleans solutions. Instead, we’ll keep it simple so that in a short time we have a starting point to explore what Orleans has to offer.

Tip: Use Ctrl+. (Control dot) to resolve namespaces in Visual Studio.

The source code for this article is the Orleans2GettingStarted folder in the Gigi Labs BitBucket repository.

.NET Core Project

To demonstrate the fact that Orleans 2.0 really supports .NET Core, we’ll create a .NET Core console app and set up everything in it. To keep things simple, we’ll run both the client and the silo (server) from this same application.

Install Packages

There are a few packages we’ll need:

Install-Package Microsoft.Orleans.Server
Install-Package Microsoft.Orleans.Client
Install-Package Microsoft.Orleans.OrleansCodeGenerator.Build
Install-Package Microsoft.Extensions.Logging.Console
Install-Package OrleansDashboard

Here is a summary of what each of these does:

  1. Used by Orleans silo (server).
  2. Used by Orleans client.
  3. Required for build-time code generation. Bad things happen if you don’t include it.
  4. Optional, but allows us to set up logging to console to see what Orleans is doing.
  5. Optional, but allows us to visualise the operation of silos and grains.

Grain and Grain Interface

We’ll add a simple grain class to the project in order to have a minimal example. Since grains are independent of each other, Internet of Things (IoT) scenarios fit very nicely. Imagine we have a number of temperature sensors deployed in different places. Each one has an ID, and periodically submits temperature readings to a corresponding grain in the Orleans cluster:

    public class TemperatureSensorGrain : Grain, ITemperatureSensorGrain
    {
        public Task SubmitTemperatureAsync(float temperature)
        {
            long grainId = this.GetPrimaryKeyLong();
            Console.WriteLine($"{grainId} received temperature: {temperature}");

            return Task.CompletedTask;
        }
    }

We’re not doing anything special here. We write out the grain ID and the value we received just so we see something going on in the console. It is important that we inherit from the Grain base class, and that all our methods return Task.

We also need a grain interface:

    public interface ITemperatureSensorGrain : IGrainWithIntegerKey
    {
        Task SubmitTemperatureAsync(float temperature);
    }

The interface must inherit from an Orleans-defined interface that tells what type of grain ID (key) it will use. Our grains will have an ID of type long (that’s some misleading naming in the interface), but there are other options including GUID or string.

Silo

Taking a look at the Hello World sample gives an idea of how to set up minimal silo and client. Since this code is going to be async, we’ll need use C# 7.1+ (to support async/await in Main()) or use a workaround. See the last section of “Working with Asynchronous Methods in C#” for how this is done (quick tip: Project Properties -> Build -> Advanced… -> C# latest minor version (latest)).

We can adapt the code from the Hello World sample to run a simple silo:

        static async Task Main(string[] args)
        {
            var siloBuilder = new SiloHostBuilder()
                .UseLocalhostClustering()
                .Configure<ClusterOptions>(options =>
                {
                    options.ClusterId = "dev";
                    options.ServiceId = "Orleans2GettingStarted";
                })
                .Configure<EndpointOptions>(options =>
                    options.AdvertisedIPAddress = IPAddress.Loopback)
                .ConfigureLogging(logging => logging.AddConsole());

            using (var host = siloBuilder.Build())
            {
                await host.StartAsync();

                Console.ReadLine();
            }
        }

Here, we are setting up a local cluster for development. Thanks to the console logging package we installed earlier and the ConfigureLogging() call above, we can see what Orleans is up to:

What is being written out is not important at this stage. The important thing is that the Orleans silo is running.

Client

The same Hello World sample also shows us how to set up a client that connects to the silo. This usually serves as a gateway between the outside world and the Orleans cluster. It could be a Web API, Windows service, etc; but here it will just be in the same console app as the silo.

We’ll wait to start the client after the silo has started. This is easy to do in our case because both are in the same application.

            using (var host = siloBuilder.Build())
            {
                await host.StartAsync();

                var clientBuilder = new ClientBuilder()
                    .UseLocalhostClustering()
                    .Configure<ClusterOptions>(options =>
                    {
                        options.ClusterId = "dev";
                        options.ServiceId = "Orleans2GettingStarted";
                    })
                    .ConfigureLogging(logging => logging.AddConsole());

                using (var client = clientBuilder.Build())
                {
                    await client.Connect();

                    var sensor = client.GetGrain<ITemperatureSensorGrain>(123);
                    await sensor.SubmitTemperatureAsync(32.5f);

                    Console.ReadLine();
                }
            }

The setup for the client is very similar to that of the silo, and quite straightforward since we are using the default localhost configurations. One thing you’ll notice is the unfortunate inconsistent naming between host.StartAsync() and client.Connect(); the latter lacks the –Async() suffix, even though it also returns a Task.

If we run this code, we see that the code in the grain is getting executed, and we see the temperature reading in the console at the end:

Dashboard

Although it works, this example is really boring. We essentially have a Hello World here, styled for IoT. Let’s change the client code to generate some load instead:

                using (var client = clientBuilder.Build())
                {
                    await client.Connect();

                    var random = new Random();
                    string sky = "blue";

                    while (sky == "blue") // if run in Ireland, it exits loop immediately
                    {
                        int grainId = random.Next(0, 500);
                        double temperature = random.NextDouble() * 40;
                        var sensor = client.GetGrain<ITemperatureSensorGrain>(grainId);
                        await sensor.SubmitTemperatureAsync((float)temperature);
                    }
                }

Now we can see the silo brimming with activity, but only in the console:

Now, wouldn’t it be nice if we could see some graphs showing what our grains and silos are doing? As it turns out, we can do that by setting up the Orleans Dashboard, a community-contributed admin dashboard for Microsoft Orleans.

We’ve already installed the package for it, so all we need to do is add it to the silo configuration:

            var siloBuilder = new SiloHostBuilder()
                .UseLocalhostClustering()
                .UseDashboard(options => { })
                .Configure<ClusterOptions>(options =>
                {
                    options.ClusterId = "dev";
                    options.ServiceId = "Orleans2GettingStarted";
                })
                .Configure<EndpointOptions>(options =>
                    options.AdvertisedIPAddress = IPAddress.Loopback)
                .ConfigureLogging(logging => logging.AddConsole());

That sets up the dashboard with all default values (port 8080, no username/password) which you can always change if you need to.

So now, if we run the application again and open localhost:8080 in a browser window, we can get some pretty visualisations.

Here’s the high-level view of the cluster:

And here’s a view of the grains that are running. You’ll see we have 500 instances of our TemperatureSensorGrain, which corresponds to the range of grainIds we’re generating at random as we generate load. You’ll also see some internal system-related grains:

Here’s a view of the grain itself, and the methods being called on it:

We can also get a view of the silo:

We haven’t covered everything the dashboard gives you, but you can already see that it gives a lot of visibility into what’s going on. It’s great to track errors, slow requests, throughput, etc.

Linux

So now we have Orleans in a .NET Core project on Windows. That’s great, but the real benefit of Orleans supporting .NET Core is cross-platform deployment. So after installing .NET Core on a Linux machine (I’m using Ubuntu 17.10.1 here), let’s grab the code and run Orleans:

git clone https://bitbucket.org/dandago/gigilabs.git
cd gigilabs
cd Orleans2GettingStarted
cd Orleans2GettingStarted
dotnet run

Orleans has no problem starting up on this Ubuntu VM:

And here we can see Orleans running with the Orleans Dashboard in the background:

Summary

Orleans 2.0 is based on .NET Standard 2.0, so Orleans can now run on .NET Core and the full .NET Framework alike. It can be run on any platform capable of running .NET Core, Linux being just one example.

A big thanks goes to the Microsoft Orleans team for making this happen! (And to Richard Astbury for the awesome Orleans Dashboard, which he still claims is alpha quality.)

To recap: in order to have a minimal Orleans sample running, we need to:

  1. Install the necessary packages.
  2. Add a grain and a grain interface.
  3. Set up the silo and client.
  4. Use the grain from the client.
  5. Optionally, set up logging and the Orleans Dashboard.

This example is meant to get you quickly up and running, and does not delve into any proper project structure or optimisations, which you would normally have when building a serious solution around Orleans. In the next Orleans 2.0 article, we’ll see how to properly organise an Orleans 2.0 solution.

One thought on “Getting Started with Microsoft Orleans 2.0 in .NET Core”

Leave a Reply

Your email address will not be published. Required fields are marked *