Recently I worked a discovery piece of work where I had to add MS Orleans to an existing WebAPI application. MS Orleans supports different types of storage, including Azure Table, SQL server and DynamoDb. I wanted to use DynamoDb and what I found was there weren’t a lot of documentation on how to add MS Orleans to an existing WebAPI and almost close to nothing when it comes to setting it up to use DynamoDb. With a lot of trials and errors, I had it set up and in this post I’m going to show the steps needed to get it going.
In the WebAPI application, add the following nuget packages
Microsoft.Orleans.Clustering.DynamoDB
Microsoft.Orleans.Persistence.DynamoDB
Microsoft.Orleans.Server
Microsoft.Orleans.CodeGenerator.MSBuild
Microsoft.Orleans.Core
Then, we add a sample Grain, DocumentGrain, as an example
[Serializable]
public class DocumentState
{
public DateTime UpdatedTimeStamp { get; set; }
}
public interface IDocumentGrain : IGrainWithStringKey
{
Task UpdateDocument();
Task<DateTime> GetUpdatedTimeStamp();
}
public class DocumentGrain : Grain<DocumentState>, IDocumentGrain
{
public Task<DateTime> GetUpdatedTimeStamp()
=> Task.FromResult(State.UpdatedTimeStamp);
public async Task UpdateDocument()
{
State.UpdatedTimeStamp = DateTime.UtcNow;
// Other logic goes here
await WriteStateAsync();
}
}
Next, in Program.cs, change CreateHostBuilder as below to add the configuration for MS Orleans
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.UseOrleans((hostBuilder, siloBuilder) =>
{
var awsServiceName = "ap-southeast-2"; // This is your AWS region or use http://localhost:{portnumber} if you use localstack
siloBuilder
.ConfigureApplicationParts(apm => apm
.AddApplicationPart(typeof(DocumentGrain).Assembly)
.WithReferences())
.UseDynamoDBClustering(config =>
{
config.Service = awsServiceName;
config.TableName = "OrleansSilos";
// If you don't want to use the default AWS profile,
// you can specify the access key and secret key
//config.AccessKey = "your access key here";
//config.SecretKey = "you secret key here";
})
.AddDynamoDBGrainStorageAsDefault(config =>
{
config.TableName = "OrleansGrainState";
config.Service = awsServiceName;
config.CreateIfNotExists = true;
config.UpdateIfExists = true;
config.UseJson = true;
// If you don't want to use the default AWS profile,
// you can specify the access key and secret key
//config.AccessKey = "your access key here";
//config.SecretKey = "you secret key here";
})
.Configure<ClusterOptions>(options =>
{
options.ClusterId = $"dev-{Guid.NewGuid()}";
options.ServiceId = "SampleMsOrleans";
})
.ConfigureLogging(log => log.AddConsole());
});
That’s it in terms of configuration. WebAPI will be using IClusterClient to get the Grain and since it’s also the silo host (same process), we can just inject the cluster client via constructor and it’ll pass in the silo builder’s cluster client.
[ApiController]
[Route("[controller]")]
public class SampleController : ControllerBase
{
private readonly IClusterClient _client;
public SampleController(IClusterClient client)
{
_client = client;
}
[HttpGet]
public async Task<IActionResult> Get(string documentId)
{
var documentGrain = _client.GetGrain<IDocumentGrain>(documentId);
var updatedTimestamp = await documentGrain.GetUpdatedTimeStamp();
await documentGrain.UpdateDocument();
return Ok();
}
}
First time, the update timestamp is DateTime.Min