Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

How to implement DI support in your Orleans Silo

0.00/5 (No votes)
12 May 2016 1  
This article describes how to implement DI support in an Orleans Silo.

Introduction

As of version 1.1.0 Orleans has supported ASP.NET vNext style dependency injection support.  However precious little has actually been documented on how to actually leverage it within an Orleans silo.  This article is intended to provide a complete step-by-step guide to integrating DI support into a silo from start to finish.  This article is not a tutorial on Orleans itself, nor on Castle Windsor.  A cursory knowledge in both is assumed.  Also, although this article focuses on integration with Castle, substituting any other DI container (e.g. AutoFac, StructureMap, Ninject, etc...) should be fairly trivial.

Disclaimer

It should be mentioned that I'm a complete beginner when it comes to Orleans and still learning.  If anyone spots anything glaringly wrong here please point it out so I can fix it.  This is largely a chronicle of my research into wiring up a DI system into Orleans.

Using the code

Initially we're going to create a standard three-project solution without DI support.  Once everything is wired up then we will update the project to support DI via Castle Windsor.  This is a bit long-winded, but I wanted to be as thorough as possible and not miss anything.

Step 1 - Create the Projects

The first thing we're going to do is create our projects.  The first project (ClientApp) will be an Orleans Dev/Test Host.  The second (Grains) will be a Orleans Grain Class Collection.  The final (Grains.Interfaces) will be a Orleans Grain Interface Collection.  Once the projects are created and referenced it should look like the following screen shots (note: I deleted the sample code from the projects).

Step 2 - Add Orleans.Server to the Grains Project

We want to host our silo in a separate process.  The easiest way to do this is to add the nuget package 'Microsoft.Orleans.Server' to the Grains project.  This includes the OrleansHost.exe which makes it easy to spin up a silo for our grains.  The simplest way to do this is to add the package via powershell:

Install-Package Microsoft.Orleans.Server -ProjectName Grains

Step 3 - Configure the Client and Host

We need to add two configuration files.  A client configuration (ClientConfiguration.xml) to the ClientApp project, and a server configuration (OrleansConfiguration.xml) to the Grains project.  Both should be set to content/copy if newer.

The OrleansConfiguration.xml (Grains) should look like this:

<?xml version="1.0" encoding="utf-8"?>
<OrleansConfiguration xmlns="urn:orleans">
  <Globals>
    <SeedNode Address="localhost" Port="10000" />
  </Globals>
  <Defaults>
    <Networking Address="localhost" Port="10000" />
    <ProxyingGateway Address="localhost" Port="30000" />
  </Defaults>
</OrleansConfiguration>

The ClientConfiguration.xml (ClientApp) should look like this:

<?xml version="1.0" encoding="utf-8" ?>
<ClientConfiguration xmlns="urn:orleans">
  <Gateway Address="localhost" Port="30000" />
</ClientConfiguration>

Step 4 - Configure Visual Studio to Launch both Projects Simultaneously (Optional)

This is an optional step.  If this step is skipped then OrleansHost.exe will need to be started manually before starting the ClientApp project.

While configuring a Visual Studio solution to launch multiple projects itself is trivial, since our Grains project is not an executable we need to tell Visual Studio what to actually run.  The easiest way to do this would be to go to Project Properties > Debug Tab > Start External Program.  However when going this route you must put the full path to the executable in the text box.  This could cause problems should the solution be copied to another folder (or if someone else downloaded it onto their computer). A more robust solution is to use the $(SolutionDir) macro when setting the path to the OrleansHost.exe file.  Unfortunately this can't be done within Visual Studio itself since the UI does not support macros.  To do this we need to modify the csproj file itself.  Open the Grains.csproj and modify the <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> element to contain this information (see below: highlighted).  Visual Studio will automatically expand this for you when the project loads.

  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <Prefer32Bit>false</Prefer32Bit>
    <StartAction>Program</StartAction>
    <StartProgram>$(SolutionDir)Grains\bin\Debug\OrleansHost.exe</StartProgram>
    <StartWorkingDirectory>
    </StartWorkingDirectory>
  </PropertyGroup>

Once this is done and the project has reloaded we simply need to right click the Solution > Properties > Startup Project and select "Multiple Startup Projects" (see below)

Step 5 - Write Boilerplate Code to Start ClientApp

Now that the projects are (mostly) in place lets return to Program.cs in ClientApp and modify it so that it can connect up to our Grain silo.  Delete all of the code and replace it with the following:

using System;
using Orleans;

namespace ClientApp
{
    public class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Press any key once the silo has started.");
            Console.ReadKey(intercept: true);
            GrainClient.Initialize("ClientConfiguration.xml");

            // TODO: our grains will go here later.

            Console.WriteLine("Press any key to exit.");
            Console.ReadKey(intercept: true);
        }
    }
}

Lets go through this piece by piece.

Console.WriteLine("Press any key once the silo has started.");
Console.ReadKey(intercept: true);

Since OrleansHost may not be fully loaded when we start ClientApp we want to introduce a pause in the application to give it a chance to start up.  If one were to try to start OrleansHost and ClientApp in the same time then the ClientApp will likely crash since the host won't be alive by the time it attempts to connect to it (i.e. a race condition between two processes).

GrainClient.Initialize("ClientConfiguration.xml");

This should be self-explanatory.  Essentially we're configuring our client using the ClientConfiguration.xml we wrote earlier.

The rest we'll fill in later.

At this point if you run the solution both OrleansHost and ClientApp should load successfully.  They won't actually do anything.  Let's do something about that!

Step 6 - Create Some Grains for our Silo

Now that our projects are created, wired up, and actually talking to each other, let's create a grain so we can do some meaningful work.  In this example we're going to the oft-used calculator example.  Remember, the first round will not use DI at all.  Once our grains are in place then we'll refactor the project to support DI.

Lets start with the ICalculatorGrain.  This goes in Grains.Interfaces, and should look like this:

using System.Threading.Tasks;
using Orleans;

namespace Grains.Interfaces
{
    public interface ICalculatorGrain : IGrainWithGuidKey
    {
        Task<int> Add(int a, int b);
        Task<int> Sub(int a, int b);
        Task<int> Mul(int a, int b);
        Task<int> Div(int a, int b);
    }
}

The equally boring CalculatorGrain in Grains should look like this:

using System.Threading.Tasks;
using Grains.Interfaces;
using Orleans;

namespace Grains
{
    public class CalculatorGrain : Grain, ICalculatorGrain
    {
        public Task<int> Add(int a, int b) => Task.FromResult(a + b);
        public Task<int> Sub(int a, int b) => Task.FromResult(a - b);
        public Task<int> Mul(int a, int b) => Task.FromResult(a * b);
        public Task<int> Div(int a, int b) => Task.FromResult(a / b);
    }
}

Finally, let's return to the Main() method in ClientApp and replace our // TODO section with a little sample code that exercises this functionality.

var grain = GrainClient.GrainFactory.GetGrain<ICalculatorGrain>(Guid.NewGuid());

Console.WriteLine($"1 + 2 = {grain.Add(1, 2).Result}");
Console.WriteLine($"2 - 3 = {grain.Sub(2, 3).Result}");
Console.WriteLine($"3 * 4 = {grain.Mul(3, 4).Result}");
Console.WriteLine($"4 / 2 = {grain.Div(4, 2).Result}");

If we run the solution now we should get the following output:

Everything works. Awesome!  Now what about DI support?

Step 7 - Introducing DI into the Grains Project

Before we go any further we need to add two nuget references to our Grains project.  The first is Castle Windsor (or whichever DI container you prefer).  The second is Microsoft's Extension Dependeny Injection framework.  From powershell execute the following commands:

Install-Package Castle.Windsor -ProjectName Grains
Install-Package Microsoft.Extensions.DependencyInjection -ProjectName Grains -Pre

Note the -Pre argument to Microsoft.Extensions.DependencyInjection.  At the time of writing this project is still considered pre-release.

Step 8 - A quick detour into how Orleans DI works

Before going further it's probably a good idea to explain what Orleans is doing behind the scenes when it creates your grains.  When Orleans initially brings a grain into existence, it uses an IServiceProvider interface to actually instantiate the object.  This is a very simple interface with only a single method:

object GetService(Type serviceType);

By default (as of 1.2.0 at least): The default service provider used by Orleans simply uses the Activator class to instantiate objects. For example:

public class DefaultServiceProvider : IServiceProvider
{
    public object GetService(Type serviceType)
    {
        return Activator.CreateInstance(serviceType);
    }
}

Using just this information it should be clear that the first step here is to create an implementation of IServiceProvider that does resolution through a container.  Let's do that now:

using System;
using Castle.MicroKernel;

namespace Grains
{
    public class CastleServiceProvider : IServiceProvider
    {
        private readonly IKernel kernel;

        public CastleServiceProvider(IKernel kernel)
        {
            this.kernel = kernel;
        }

        public object GetService(Type serviceType) => this.kernel.Resolve(serviceType);
    }
}

That was easy -- but where do we actually stick this?  Digging into the Orleans source you'll notice the class ConfigureStartupBuilder in the Orleans.DependencyInjection project.  Without delving too deeply into the file it's essentially invoked with a single parameter: startupTypeName (more on this later).  The startup builder essentially interrogates this type and looks for a method in it called "ConfigureServices" which takes a parameter of type IServiceCollection and returns a type IServiceProvider.  If you study the code you'll notice that by default if no startupTypeName is configured then it will simply return an instance of the previously mentioned DefaultServiceProvider.

Using this information lets create a class that implements this method:

using System;
using Castle.MicroKernel.Registration;
using Castle.Windsor;
using Grains.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Orleans;

namespace Grains
{
    public class Startup
    {
        public IServiceProvider ConfigureServices(IServiceCollection services)
        {
            var container = new WindsorContainer();

            // Scan for all of the grains in this assembly.
            container.Register(Classes.FromThisAssembly().BasedOn<Grain>().LifestyleTransient());

            // TODO: Register our custom providers here.

            foreach (var service in services)
            {
                switch (service.Lifetime)
                {
                    case ServiceLifetime.Singleton:
                        container.Register(
                            Component
                                .For(service.ServiceType)
                                .ImplementedBy(service.ImplementationType)
                                .LifestyleSingleton());
                        break;
                    case ServiceLifetime.Transient:
                        container.Register(
                            Component
                                .For(service.ServiceType)
                                .ImplementedBy(service.ImplementationType)
                                .LifestyleTransient());
                        break;
                    case ServiceLifetime.Scoped:
                        var error = $"Scoped lifestyle not supported for '{service.ServiceType.Name}'.";
                        throw new InvalidOperationException(error);
                }
            }

            return new CastleServiceProvider(container.Kernel);
        }
    }
}

Let's go through this line by line:

The first thing we need to do is create our container and then register all of our grain implementations against this.  The Classes.FromThisAssembly() is a convenient way to register types en-mass in Castle Windsor.

var container = new WindsorContainer();

// Scan for all of the grains in this assembly.
container.Register(Classes.FromThisAssembly().BasedOn<Grain>().LifestyleTransient());

When Orleans calls our ConfigureServices() method it will pass in a list of type mappings that it wants to be registered automatically, so we need to register them into the container.  I put an exception in the 'Scoped' block because I'm not sure how how to handle this (still learning).  Also as of the time of the current writing Orleans doesn't register anything this way.  In fact, everything it registers is transient.

foreach (var service in services)
{
    switch (service.Lifetime)
    {
        case ServiceLifetime.Singleton:
            container.Register(
                Component
                    .For(service.ServiceType)
                    .ImplementedBy(service.ImplementationType)
                    .LifestyleSingleton());
            break;
        case ServiceLifetime.Transient:
            container.Register(
                Component
                    .For(service.ServiceType)
                    .ImplementedBy(service.ImplementationType)
                    .LifestyleTransient());
            break;
        case ServiceLifetime.Scoped:
            throw new InvalidOperationException($"Scoped lifestyle not supported '{service.ServiceType.Name}'.");
    }
}

Finally, we pass the container's kernel to the constructor to our CastleServiceProvider class and return it.  Now whenever Orleans tries to create something it will use our provider instead.

return new CastleServiceProvider(container.Kernel);

Now, what about that startupTypeName parameter?  Where does it go?  In OrleansConfiguration.xml!

Lets open up our OrleansConfiguration.xml and add it to the <Defaults /> section:

<?xml version="1.0" encoding="utf-8"?>
<OrleansConfiguration xmlns="urn:orleans">
  <Globals>
    <SeedNode Address="localhost" Port="10000" />
  </Globals>
  <Defaults>
    <Startup Type="Grains.Startup, Grains" />
    <Networking Address="localhost" Port="10000" />
    <ProxyingGateway Address="localhost" Port="30000" />
  </Defaults>
</OrleansConfiguration>

Note: you may notice an intellisense warning here that 'Startup' is not a valid element type; ignore it.  It's not part of the OrleansConfiguration.xsd file, but it's recognized by Orleans itself.

At this point you can run the solution again.  The behavior should be identical to before.  If you're curious go ahead and set a breakpoint within the Startup class to confirm that it is in fact being called by Orleans to create your grains.

Step 9 - Adding Dependencies to our Grains (Finally)

To recap: we have three projects in our solution, a client app, an interface project, and a grain implementation that's running our silo.  We've created a simple calculator grain, and we've wired Castle Windsor into everything so we can now utilize ctor injection when instantiating grains (or property injection).  The last part of the tutorial is standard fare for anyone familiar with DI.

First lets create an interface for our 'new' calculator provider in the Grains.Interfaces project:

namespace Grains.Interfaces
{
    public interface ICalculatorProvider
    {
        int Add(int a, int b);
        int Sub(int a, int b);
        int Mul(int a, int b);
        int Div(int a, int b);
    }
}

Second, we'll create an implementation of it in the Grains project:

using Grains.Interfaces;

namespace Grains
{
    public class CalculatorProvider : ICalculatorProvider
    {
        public int Add(int a, int b) => a + b;
        public int Div(int a, int b) => a / b;
        public int Mul(int a, int b) => a * b;
        public int Sub(int a, int b) => a - b;
    }
}

Third, let's re-write our CalculatorGrain so that it takes our ICalculatorProvider as a dependency and uses that instead:

using System.Threading.Tasks;
using Grains.Interfaces;
using Orleans;

namespace Grains
{
    public class CalculatorGrain : Grain, ICalculatorGrain
    {
        private readonly ICalculatorProvider calc;

        public CalculatorGrain(ICalculatorProvider calc)
        {
            this.calc = calc;
        }

        public Task<int> Add(int a, int b) => Task.FromResult(calc.Add(a, b));
        public Task<int> Div(int a, int b) => Task.FromResult(calc.Div(a, b));
        public Task<int> Mul(int a, int b) => Task.FromResult(calc.Mul(a, b));
        public Task<int> Sub(int a, int b) => Task.FromResult(calc.Sub(a, b));
    }
}

Finally, we need to register our provider with the container.  In a normal application we would probably use an IWindsorInstaller to do this, but to keep things simple we'll just replace the // TODO portion of the Startup.ConfigureServices() method to do this by hand:

container.Register(Component.For<ICalculatorProvider>().ImplementedBy<CalculatorProvider>().LifestyleTransient());

And...that's it! Go ahead and run the solution to ensure that it still works.

Enjoy!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here