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:
Dependency injection in ASP.NET Core
How do we apply this to Orleans?
We can demonstrate this dependency injection concept in Orleans by building a new IOrleansFunction
of course! Note that this functionality was created for my Orleans series in:
Updating Orleans Project to be more ready for new Orleans Examples!
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!
Code at this point is in this release on the GitHub repository:
Kritner-Blogs/OrleansGettingStarted
Related:
Top comments (0)