Microsoft Orleans is an actor model implementation for the .NET platform. The main idea of an actor model is that you distribute your computation across a number of fine-grained elements (called actors, or in Orleans, grains) which communicate between each other using message passing, and can each execute only one message at a time. This altogether avoids any need for multithreading synchronisation, and promotes distribution of computation even across physical machines.
The Orleans team have gone to great lengths to develop an API that is friendly to .NET developers, and which hides much of the complexity of building distributed systems. In this article, I hope to get you up and running with Orleans in no time.
The source code for the latter part of this article is available at the Gigi Labs BitBucket repository.
The Visual Studio Extension
If you download and install the Microsoft Orleans Tools for Visual Studio extension, you’ll get a few project templates that you can use to easily create new Orleans projects:
This is not strictly necessary, and I will show you how to create Orleans projects without it. But it’s quite convenient to have.
The Dev/Test Host project
A really quick way to start playing with Microsoft Orleans is to create a project of type Orleans Dev/Test Host. This is one of the project types provided by the Visual Studio extension (see previous section), and it will generate a bunch of infrastructure code (Program.cs and OrleansHostWrapper.cs) that you can use as-is. In fact, just go ahead and run it:
Just give it a few seconds; Orleans will perform some initialisation tasks, and then tell you that you have a silo running. Orleans applications typically involve a client (a regular console application, Web API, etc which communicates with Orleans) and a server (known as a silo, where all the Orleans magic happens). The Orleans Dev/Test Host is simply a way of having both in the same project.
Hello World
Among the code generated with the Orleans Dev/Test Host template, you’ll find a Main()
method that looks something like this:
static void Main(string[] args) { // The Orleans silo environment is initialized in its own app domain in order to more // closely emulate the distributed situation, when the client and the server cannot // pass data via shared memory. AppDomain hostDomain = AppDomain.CreateDomain("OrleansHost", null, new AppDomainSetup { AppDomainInitializer = InitSilo, AppDomainInitializerArguments = args, }); var config = ClientConfiguration.LocalhostSilo(); GrainClient.Initialize(config); // TODO: once the previous call returns, the silo is up and running. // This is the place your custom logic, for example calling client logic // or initializing an HTTP front end for accepting incoming requests. Console.WriteLine("Orleans Silo is running.\nPress Enter to terminate..."); Console.ReadLine(); hostDomain.DoCallBack(ShutdownSilo); }
We can put some of our own code instead of that comment. For this, we’ll need to create an interface:
public interface IPerson : IGrainWithStringKey { Task SayHelloAsync(string message); }
One important thing to note at this stage is that interfaces used by Orleans grains must always return a Task
(or Task<T>
), because Orleans enforces asynchrony on its actors/grains.
Another thing to note is the IGrainWithStringKey
interface we’re extending. All grains have an identifier (that’s how we’re able to find them from our code); identifiers may be of type string
(as in this case), Guid
, long
, or a special compound type.
Next, we can create a class that implements this interface:
public class PersonGrain : Grain, IPerson { public Task SayHelloAsync(string message) { string name = this.GetPrimaryKeyString(); Console.WriteLine($"{name} says: {message}"); return TaskDone.Done; } }
We’re deriving our class from Orleans’ Grain
type, and we’re implementing the interface we just declared. We’re getting the grain’s identifier using the GetPrimaryKeyString()
method before writing out a message. The TaskDone.Done
is simply a utility task that you can return from non-async
methods. Similar to the Task.CompletedTask that was introduced in .NET 4.6, this is just a convenient way to avoid doing something like Task.FromResult(0)
all the time.
With that done, all we need to do is replace the comment in Main()
with code such as the following:
var joe = GrainClient.GrainFactory.GetGrain<IPerson>("Joe"); joe.SayHelloAsync("Hello!");
This gets an instance of a grain that implements IPerson
, with identifier “Joe”. The grain will be created if it doesn’t already exist; Orleans abstracts out the creation and destruction of actors. Once we have an instance, we can call methods on it. They are all asynchronous, but whether you will await
them depends very much on the type of application you will be writing.
This is already enough for you to go on experimenting with Orleans. In the next sections (which you can safely skip if you’re just starting out), I will explain how to build out a proper project structure.
Grain Class and Grain Interface projects
There are two other project types you get with the Visual Studio extension: Orleans Grain Class Collection, and Orleans Grain Interface Collection. As your project starts to grow, you should move your grain classes and interfaces out into these project types, so that your main application consists solely of client code that interacts with Orleans via the GrainClient.GrainFactory
.
NuGet packages
If you don’t want to use the Visual Studio extension, you can create regular class libraries or console applications, and just add in the necessary NuGet packages. These are the projects you need to create and the NuGet packages you need to install for each:
Purpose | Type | NuGet package | References |
---|---|---|---|
Client | Console Application | Microsoft.Orleans.Client | Interfaces |
Silo | Console Application | Microsoft.Orleans.Server | Interfaces, Grains |
Grains | Class Library | Microsoft.Orleans.Core Microsoft.Orleans.OrleansCodeGenerator.Build |
Interfaces |
Interfaces | Class Library | Microsoft.Orleans.Core Microsoft.Orleans.OrleansCodeGenerator.Build |
While the Microsoft.Orleans.OrleansCodeGenerator.Build
package is not strictly necessary, it enables code generation for grains and grain interfaces at build time, which is better than having to do it at runtime.
A More Complete Project Structure
The rest of what we need to do to make this application work (whether using the Visual Studio extension or not) is described in the Minimal Orleans Application tutorial. I’m going to adapt it so that we have separate projects for client and silo/host (which is usually the case in real applications), to use XML configuration throughout, to fit our earlier IPerson
example, and to retry to connect from the client to the silo until the silo is up.
The Silo needs the following Garbage Collection configuration to go into its App.config:
<configuration> <runtime> <gcServer enabled="true"/> <gcConcurrent enabled="false"/> </runtime> </configuration>
Add a file called OrleansConfiguration.xml to the Silo and set it to Copy always:
<?xml version="1.0" encoding="utf-8"?> <OrleansConfiguration xmlns="urn:orleans"> <Globals> <SeedNode Address="localhost" Port="11111" /> </Globals> <Defaults> <Networking Address="localhost" Port="11111" /> <ProxyingGateway Address="localhost" Port="30000" /> </Defaults> </OrleansConfiguration>
Silo configuration needs (at a minimum) two things: an endpoint for inter-silo communication (that’s the Networking
element) and an endpoint that clients outside the silo can use to talk to it (ProxyingGateway
). In other words, our Client needs to talk to the Silo on port 30000.
In fact, we will now add a ClientConfiguration.xml
file to the Client (set this also to Copy always):
<ClientConfiguration xmlns="urn:orleans"> <Gateway Address="localhost" Port="30000"/> </ClientConfiguration>
The PersonGrain
class we saw earlier needs to go in the Grains project, and the IPerson
interface needs to go in the Interfaces project.
Our program logic will look something like this (basically adapted from the Minimal Orleans Application tutorial, but without the client code):
class Program { static SiloHost siloHost; static void Main(string[] args) { Console.Title = "Silo"; // Orleans should run in its own AppDomain, we set it up like this AppDomain hostDomain = AppDomain.CreateDomain("OrleansHost", null, new AppDomainSetup() { AppDomainInitializer = InitSilo }); Console.WriteLine("Orleans Silo is running.\nPress Enter to terminate..."); Console.ReadLine(); // We do a clean shutdown in the other AppDomain hostDomain.DoCallBack(ShutdownSilo); } static void InitSilo(string[] args) { siloHost = new SiloHost(Dns.GetHostName()); siloHost.LoadOrleansConfig(); siloHost.InitializeOrleansSilo(); var startedok = siloHost.StartOrleansSilo(); if (!startedok) throw new SystemException(String.Format("Failed to start Orleans silo '{0}' as a {1} node", siloHost.Name, siloHost.Type)); } static void ShutdownSilo() { if (siloHost != null) { siloHost.Dispose(); GC.SuppressFinalize(siloHost); siloHost = null; } } }
Program logic in the Client should look something like this:
static void Main(string[] args) { Console.Title = "Client"; var config = ClientConfiguration.LoadFromFile("ClientConfiguration.xml"); while (true) { try { GrainClient.Initialize(config); Console.WriteLine("Connected to silo!"); var friend = GrainClient.GrainFactory.GetGrain<IPerson>("Joe"); var result = friend.SayHelloAsync("Hello!"); Console.ReadLine(); break; } catch (SiloUnavailableException) { Console.WriteLine("Silo not available! Retrying in 3 seconds."); Thread.Sleep(3000); } } }
Set both the Silo and Client as startup projects, and run them. The silo will take a few seconds to initialise, and the client will keep trying to reach it until it’s up:
Interesting article. I’d love to learn more about Orleans, especially how scale it on Azure. Does Orleans have the ability to spin up / down new silos (or grains within a silo) based on current load?
That’s an interesting question. Orleans is actually built with Azure in mind as the primary platform (although it can be run just as well in-house).
I don’t know whether it can actually spin up silos automatically. I can tell you though that it does have the ability to balance out grains across silos, and that you can easily scale out grains within a silo using Stateless Worker grains.
If you would like some real data on how it performs, I suggest you check out their 2014 Technical Report.