Dependency Injection (DI) has become a cornerstone of any well-designed and testable application nowadays, and Microsoft Orleans applications are no exception. In the 2.0 release, Microsoft Orleans has replaced some of its old internal frameworks (such as logging and dependency injection) with the corresponding Microsoft packages; thus these will be familiar for those who already worked with ASP .NET Core.
In this article we’ll focus on setting up dependency injection in the silo so that we can pass dependencies into our grains. However, if you read the dependency injection documentation page for Orleans 2.0, you’ll see that you can also have DI on the client side.
The source code for this article is the Orleans2DependencyInjection folder at the Gigi Labs BitBucket repository. Be careful not to confuse it with the OrleansDependencyInjection folder which targets Orleans 1.4.x.
TL;DR if you just want to quickly see how to do DI without going through the whole example, jump to the Registering Dependencies section.
Update 30th June 2018: The source code for this article needs a little adjusting, in order to gracefully stop the silo and gracefully close the client. Counterintuitively, directly disposing a silo or client is non-graceful and is generally discouraged.
Grain
We’ll start off with a project structure based on the Getting Organised article. Once that is in place, we can build an example representing a blog’s comment system. In the Grains project, we’ll add a grain representing a blog post, and that will be responsible for saving and retrieving all comments for that blog post.
public class BlogPostGrain : Grain, IBlogPostGrain { private ICommentRepository repo; private ITimeService time; public BlogPostGrain(ICommentRepository repo, ITimeService time) { this.repo = repo; this.time = time; } public Task SaveCommentAsync(int blogPostId, InputComment comment) { var storedComment = new StoredComment() { Name = comment.Name, EmailAddress = comment.EmailAddress, Body = comment.Body, Timestamp = this.time.UtcNow }; return this.repo.SaveCommentAsync(blogPostId, storedComment); } public Task<List<StoredComment>> GetCommentsAsync(int blogPostId) => this.repo.GetCommentsAsync(blogPostId); }
There are a few classes and interfaces in here that we haven’t created yet, but let’s understand what we’re doing here. We have a dependency on a repository where the comments will be held (whatever that is – we don’t care about the implementation at this stage). The grain acts mostly as a pass-through to this repository for storage and retrieval of comments, but when saving, we transform it by adding a timestamp. We use different DTOs for input comments and stored comments so that it is not possible to supply a timestamp with the input data.
We also have a second dependency on something called a time service. While you could just use DateTime.UtcNow
in your code, time is typically one of the dependencies you want to factor out of your unit tests because it can affect the results. So we wrap DateTime.UtcNow
in something we can mock, just for the sake of unit tests later.
Contracts
In the Contracts project, we’ll add all our interfaces and DTOs. Let’s start with our dependencies:
public interface ITimeService { DateTime UtcNow { get; } } public interface ICommentRepository { Task SaveCommentAsync(int blogPostId, StoredComment comment); Task<List<StoredComment>> GetCommentsAsync(int blogPostId); }
Then we have our grain interface:
public interface IBlogPostGrain : IGrainWithIntegerKey { Task SaveCommentAsync(int blogPostId, InputComment comment); Task<List<StoredComment>> GetCommentsAsync(int blogPostId); }
And finally our DTOs:
public class InputComment { public string Name { get; set; } public string EmailAddress { get; set; } public string Body { get; set; } } public class StoredComment : InputComment { public DateTime Timestamp { get; set; } }
Dependency Implementations
In the Silo, we can create implementations for our dependencies.
To keep it simple, we’ll implement our repository using a ConcurrentDictionary
. This is a volatile, in-memory implementation that is for demonstration only, but it allows us to focus on what we’re doing with Orleans, rather than distracting us with store-specific details.
Note: We could also use Orleans storage providers, but that’s out of scope here.
public class MemoryCommentRepository : ICommentRepository { private ConcurrentDictionary<int, List<StoredComment>> dict; public MemoryCommentRepository() { this.dict = new ConcurrentDictionary<int, List<StoredComment>>(); } public Task<List<StoredComment>> GetCommentsAsync(int blogPostId) { this.dict.TryGetValue(blogPostId, out var comments); return Task.FromResult(comments); } public Task SaveCommentAsync(int blogPostId, StoredComment comment) { this.dict.AddOrUpdate(blogPostId, addValue: new List<StoredComment>() { comment }, updateValueFactory: (postId, commentsList) => { commentsList.Add(comment); return commentsList; }); return Task.CompletedTask; } }
The time service is really simple: it just wraps DateTime.UtcNow
.
public class TimeService : ITimeService { public DateTime UtcNow => DateTime.UtcNow; }
Registering Dependencies
All the above was setting up the example, and now we get to the part we’ve all been waiting for.
We’ll set up our silo’s code similarly to what we’ve done in the past two articles, but this time, we’ll add a call to ConfigureServices()
in order to register our dependencies:
var siloBuilder = new SiloHostBuilder() .UseLocalhostClustering() .UseDashboard(options => { }) .Configure<ClusterOptions>(options => { options.ClusterId = "dev"; options.ServiceId = "Orleans2DependencyInjection"; }) .Configure<EndpointOptions>(options => options.AdvertisedIPAddress = IPAddress.Loopback) .ConfigureServices(services => { services.AddSingleton<ITimeService, TimeService>(); services.AddSingleton<ICommentRepository, MemoryCommentRepository>(); }) .ConfigureLogging(logging => logging.AddConsole());
Note: as per the previous articles, C# 7.1 or above is needed in order to allow async
/await
in Main()
.
Since AddSingleton()
is an extension method coming from Mirosoft.Extensions.DependencyInjection (already included as a dependency of Microsoft.Orleans.Core), you’ll need to add the following for this to work:
using Microsoft.Extensions.DependencyInjection;
The API
We can complete this example by exposing the grain’s functionality via our Web API. For this, we’ll add the following controller:
[Produces("application/json")] [Route("api/BlogPosts")] public class BlogPostsController : Controller { private IClusterClient orleansClient; public BlogPostsController(IClusterClient orleansClient) { this.orleansClient = orleansClient; } [HttpGet] public Task<List<StoredComment>> Get(int blogPostId) { var grain = this.orleansClient.GetGrain<IBlogPostGrain>(blogPostId); return grain.GetCommentsAsync(blogPostId); } [HttpPut] public async Task Put(int blogPostId, InputComment comment) { var grain = this.orleansClient.GetGrain<IBlogPostGrain>(blogPostId); await grain.SaveCommentAsync(blogPostId, comment); } }
Note: as I write this, I am noticing a quirk in this implementation. If you get a grain with a blogPostId, then why do you have to pass it again to call the method on the grain? The grain should know its ID already. Fair enough – that was an oversight on my part. But since grain IDs are retrieved using extension methods, and thus their retrieval would also need to be mocked, I’d rather not overcomplicate things in this example.
We can then add Swagger to the Web API and wire up the Orleans client as we did in the Getting Organised article (complete with retries):
private IClusterClient CreateOrleansClient() { var clientBuilder = new ClientBuilder() .UseLocalhostClustering() .Configure<ClusterOptions>(options => { options.ClusterId = "dev"; options.ServiceId = "Orleans2DependencyInjection"; }) .ConfigureLogging(logging => logging.AddConsole()); var client = clientBuilder.Build(); client.Connect(async ex => { Console.WriteLine("Retrying..."); await Task.Delay(3000); return true; }).Wait(); return client; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { var orleansClient = CreateOrleansClient(); services.AddSingleton<IClusterClient>(orleansClient); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" }); }); services.AddMvc(); }
Manual Testing with Swagger
We can quickly add a couple of comments on a blog post and retrieve them to see that all this is working:
Note: seems like Swagger recently changed their UI. I liked it a lot better before.
Unit Testing
Dependency injection makes it easy for us to write unit tests. Let’s add a Grains.Tests project (.NET Core Console App), add a reference to the Grains project, and install the following packages:
Install-Package Microsoft.NET.Test.Sdk Install-Package NUnit Install-Package NUnit3TestAdapter Install-Package Moq
Remove the auto-generated Program.cs file and add the following test class instead:
public class BlogPostGrainTests { [Test] public async Task SaveCommentTest() { // arrange const int blogPostId = 1; var fixedDateTime = new DateTime(2018, 4, 29, 18, 28, 33, DateTimeKind.Utc); var mockRepo = new Mock<ICommentRepository>(MockBehavior.Strict); var mockTimeService = new Mock<ITimeService>(MockBehavior.Strict); mockRepo.Setup(x => x.SaveCommentAsync(blogPostId, It.IsAny<StoredComment>())) .Returns(Task.CompletedTask); mockTimeService.Setup(x => x.UtcNow) .Returns(fixedDateTime); var grain = new BlogPostGrain(mockRepo.Object, mockTimeService.Object); const string name = "George"; const string emailAddress = "george@food.com"; const string body = "I'm hungry!"; var comment = new InputComment() { Name = name, EmailAddress = emailAddress, Body = body }; // act await grain.SaveCommentAsync(blogPostId, comment); // assert mockRepo.Verify(x => x.SaveCommentAsync(blogPostId, It.Is<StoredComment>( c => c.Name == name && c.EmailAddress == emailAddress && c.Body == body && c.Timestamp == fixedDateTime ))); } }
This test verifies that the submitted comment was passed on to the store with the generated timestamp. It should pass:
Exercises
We’ve seen a complete example featuring dependency injection. Registering dependencies is easy; most of the effort in this article was around building the example to demonstrate that.
As you can see, you can write unit tests for grains just as you would for any other class, without having to resort to the Orleans TestCluster.
There are a number of ways you can take this further:
- Have the grain perform a validation against the email address, and write unit tests for that.
- Have the grain retrieve its own ID (removing the need to pass it as a parameter to its methods), and find a way to mock the grain retrieval.
- Try dependency injection in the Orleans client.