Microsoft Orleans — Dependency Injection

Dependency Injection is an important part of writing loosely coupled, easily tested code. I’ve written a bit about it before, but not in the context of Microsoft Orleans.


Microsoft Orleans, like most (all?) applications, can make use of dependency injection. How do we do it in Orleans? Luckily, it is accomplished in a very similar manner to what you should already be used to when working with .net core!

If you aren’t familiar with .net core DI, a quick sample:

public interface IStuffDoer
{
void DoStuff();
}
public class StuffDoer : IStuffDoer
{
public void DoStuff()
{
Console.WriteLine("Stuff has been done");
}
}

Within (generally) your Startup.cs or thereabouts:

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

// Register services for DI
services.AddSingleton<IStuffDoer, StuffDoer>();
}

And that’s pretty much all there is to it (as it relates to a MVC/WebApi site anyway). When instances of IStuffDoer are needed in class constructors, an instance is injected into the class — in this case the same instance, since we registered it as a singleton. You can read more about dependency injection here:


How do we apply this to Orleans?

Photo by Sara Bakhshi on Unsplash

We can demonstrate this dependency injection concept in Orleans by buildinga new IOrleansFunction of course! Note that this functionality was created for my Orleans series in:

First, let’s start with our non grain related code — the stuff that we’ll be using and registering with the IOC container.

An email sending interface:

public interface IEmailSender
{
Task SendEmailAsync(string from, string[] to, string subject, string body);
}

and an implementation:

public class FakeEmailSender : IEmailSender
{
private readonly ILogger<FakeEmailSender> _logger;
public FakeEmailSender(ILogger<FakeEmailSender> logger)
{
_logger = logger;
}
/// <summary>
/// Pretend this actually sends an email.
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="subject"></param>
/// <param name="body"></param>
/// <returns></returns>
public Task SendEmailAsync(string from, string[] to, string subject, string body)
{
var emailBuilder = new StringBuilder();
emailBuilder.Append("Sending new Email...");
emailBuilder.AppendLine();
emailBuilder.Append($"From: {from}");
emailBuilder.Append($"To: {string.Join(", ", to)}");
emailBuilder.Append($"Subject: {subject}");
emailBuilder.Append($"Body: {Environment.NewLine}{body}");
_logger.LogInformation(emailBuilder.ToString()); return Task.CompletedTask;
}
}

We can register this FakeEmailSender in our ISiloHostBuilder. I use a little helper class to keep all my DI registration in its own area, separate from the ISiloHostBuilder.

Helper class:

public static class DependencyInjectionHelper
{
public static void IocContainerRegistration(IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<IEmailSender, FakeEmailSender>();
}
}

Call the helper class from the ISiloHostBuilder:

.ConfigureServices(DependencyInjectionHelper.IocContainerRegistration)

The entire ISloHostBuilder method now looks like:

private static async Task<ISiloHost> StartSilo()
{
// define the cluster configuration
var builder = new SiloHostBuilder()
.UseLocalhostClustering()
.Configure<ClusterOptions>(options =>
{
options.ClusterId = "dev";
options.ServiceId = "HelloWorldApp";
})
.Configure<EndpointOptions>(options => options.AdvertisedIPAddress = IPAddress.Loopback)
.AddMemoryGrainStorage(Constants.OrleansMemoryProvider)
.ConfigureApplicationParts(parts =>
{
parts.AddApplicationPart(typeof(IGrainMarker).Assembly).WithReferences();
})
.ConfigureServices(DependencyInjectionHelper.IocContainerRegistration)
.UseDashboard(options => { })
.ConfigureLogging(logging => logging.AddConsole());
var host = builder.Build();
await host.StartAsync();
return host;
}

Now that we have a registered service, let’s use it in a grain!

I’m just going to put into place a grain that sends out an email, using out new dependency injected service. Yes, we could just write the email sending within the grain itself, but I wanted to show off dependency injection Additionally, this way we can swap in a “real” implementation w/o the (small amount of) boilerplate involved with standing up a grain.

New Grain Interface:

public interface IEmailSenderGrain : IGrainWithGuidKey, IGrainInterfaceMarker
{
Task SendEmail(string from, string[] to, string subject, string body);
}

And implementation:

public class EmailSenderGrain : Grain, IEmailSenderGrain, IGrainMarker
{
private readonly IEmailSender _emailSender;
public EmailSenderGrain(IEmailSender emailSender)
{
_emailSender = emailSender;
}
public async Task SendEmail(string from, string[] to, string subject, string body)
{
await _emailSender.SendEmailAsync(from, to, subject, body);
}
}

In the above Grain, we’re taking in an instance of an IEmailSender for use within the actual implementation of the IEmailSenderGrain contract. With the setup we did in the ISiloHostBuilder, the FakeEmailSender is passed into the class automatically.


Wire up the new grain call in the console app

Now we need to wire up the new grain call into our console app menu — luckily this is simple due to the refactor pointed out in the above blog post.

Add a new class that implements IOrleansFunction:

public class DependencyInjectionEmailService : IOrleansFunction
{
public string Description => "Shows off dependency injection within a grain implementation.";
public async Task PerformFunction(IClusterClient clusterClient)
{
var grain = clusterClient.GetGrain<IEmailSenderGrain>(Guid.NewGuid());
Console.WriteLine("Sending out a totally legit email using whatever service is registered with the IEmailSender on the SiloHost");
var body = @"
.-'---`-.
,' `.
| \
| \
\ _ \
,\ _ ,'-,/-)\
( * \ \,' ,' ,'-)
`._,) -',-')
\/ ''/
) / /
/ ,'-'
";
await grain.SendEmail(
"someDude@somePlace.com",
new[] { "someOtherDude@someOtherPlace.com" },
"ayyy lmao",
body
);
}
}

Ship it!

Let’s see what this looks like running.

  • Build
  • dotnet run the SiloHost
  • dotnet run the client
  • select whatever option it ends up being for the new grain

Output:

Note in the above that the “email” is being shown in the Orleans console, as the FakeEmailSender told it to just “log”, and from the context of where the function is running, it hits the Orleans log, rather than the menu-ed console app.

That’s all there is to it!



Russell Hammett Jr. (Kritner)

Written by

Just a boring Application Developer/Dad. I enjoy gaming, learning new technologies, reading, and potentially other stuff. That’s about it.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade