Calling Orleans from ASP.NET

In my last post Getting started with Orleans we covered a lot of ground on the basics of setting up and using Orleans. It’s quite likely you’ll be wanting to use ASP.NET as an entry point to your Orleans code, so let’s look at how we might set this up.

Create yourself an ASP.NET core project, I’m using controllers but minimal API is also fine (I just happened to have the option to use controllers selected).

After you’ve created your application, clear out the weather forecast code etc. if you created the default sample.

Add a folder for your grain(s) (mine’s named Grains, not very imaginative) and I’ve added the following files and code…

IDocumentGrain.cs

public interface IDocumentGrain : IGrainWithGuidKey
{

    Task<string> GetContent();
    Task UpdateContent(string content);
    Task<DocumentMetadata> GetMetadata();
    Task Delete();
}

DocumentGrain.cs

public class DocumentGrain([PersistentState("doc", "documentStore")] IPersistentState<DocumentState> state)
    : Grain, IDocumentGrain
{
    public Task<string> GetContent()
    {
        // State is hydrated automatically on activation
        return Task.FromResult(state.State.Content);
    }

    public async Task UpdateContent(string content)
    {
        state.State.Content = content;
        state.State.LastUpdated = DateTime.UtcNow;
        await state.WriteStateAsync(); // persist changes
    }

    public Task<DocumentMetadata> GetMetadata()
    {
        var metadata = new DocumentMetadata
        {
            Title = state.State.Title,
            LastUpdated = state.State.LastUpdated
        };
        return Task.FromResult(metadata);
    }

    public async Task Delete()
    {
        await state.ClearStateAsync(); // wipe persisted state
    }
}

DocumentMetadata.cs

[GenerateSerializer]
public class DocumentMetadata
{
    [Id(0)]
    public string Title { get; set; } = string.Empty;

    [Id(1)]
    public DateTime LastUpdated { get; set; }
}

DocumentState.cs

public class DocumentState
{
    public string Title { get; set; } = string.Empty;
    public string Content { get; set; } = string.Empty;
    public DateTime LastUpdated { get; set; }
}

Now we’ll add the DocumentController.cs in the Controllers folder

[ApiController]
[Route("api/[controller]")]
public class DocumentController(IClusterClient client) : ControllerBase
{
    [HttpGet("{id}")]
    public async Task<string> Get(Guid id)
    {
        var grain = client.GetGrain<IDocumentGrain>(id);
        return await grain.GetContent();
    }
}

Note: As we touched on in the previous post, we just use grains as if they already exist, the Orleans runtime will create and activate them if they do not exist or return them if the already exist.

Finally in Program.cs add the following code after builder.Services.AddControllers();

builder.Host.UseOrleans(silo =>
{
    silo.UseLocalhostClustering();
    silo.AddMemoryGrainStorage("documentStore");
    silo.UseDashboard(options =>
    {
        options.HostSelf = true;
        options.Port = 7000;
    });
});

When we run this application we will need to pass a GUID (as we’re using IGrainWithGuidKey) for example https://localhost:7288/api/document/B5D4A805-80C3-4239-967B-937A5A0E9250 and this will obviously send this request to the DocumentController Get endpoint and this uses a grain based upon the supplied id and calls the grain method GetContent which gets the current state Content property.

I’ve not added code to call the other methods on the grain, but examples are listed for how these might look in the code above.