Skip to content

Tutorial: Build your first Orleans app

In this tutorial, you build a small Orleans application using project boundaries typical of larger applications. Grain contracts, grain implementations, the silo, and an external client are separate projects with distinct dependencies and deployment roles.

You learn how to:

  • Define and implement a grain.
  • Host grains in a silo.
  • Connect an external client to a silo.
  • Obtain a grain reference and call it.
  • Structure project references so that clients depend on contracts, not implementations.

The manual walkthrough uses localhost clustering and transient in-memory state so that the silo and client can run directly from two terminals.

Create a solution with four Orleans projects:

  • GrainInterfaces contains grain contracts shared by callers and implementations.
  • Grains contains the grain implementations.
  • Silo hosts the Orleans runtime and grain activations.
  • Client is an external process that connects to the silo and calls grains.

The Microsoft.Orleans.Templates package creates the same Orleans project boundaries and adds an Aspire AppHost. The generated application uses Azurite-backed Azure Table clustering and Azure Blob grain storage instead of the manual walkthrough’s localhost configuration. A running container runtime lets the AppHost start Azurite:

Terminal window
dotnet new install Microsoft.Orleans.Templates
dotnet new orleans --name OrleansHelloWorld --output OrleansHelloWorld

Continue with the manual steps below to learn how the projects and references fit together.

Run the following commands in an empty directory:

Terminal window
dotnet new sln --name OrleansHelloWorld --format slnx
dotnet new classlib --name GrainInterfaces --framework net10.0
dotnet new classlib --name Grains --framework net10.0
dotnet new console --name Silo --framework net10.0
dotnet new console --name Client --framework net10.0
dotnet solution OrleansHelloWorld.slnx add GrainInterfaces/GrainInterfaces.csproj
dotnet solution OrleansHelloWorld.slnx add Grains/Grains.csproj
dotnet solution OrleansHelloWorld.slnx add Silo/Silo.csproj
dotnet solution OrleansHelloWorld.slnx add Client/Client.csproj
dotnet reference add GrainInterfaces/GrainInterfaces.csproj --project Grains/Grains.csproj
dotnet reference add Grains/Grains.csproj --project Silo/Silo.csproj
dotnet reference add GrainInterfaces/GrainInterfaces.csproj --project Client/Client.csproj
dotnet package add Microsoft.Orleans.Sdk --version 10.2.2 --project GrainInterfaces/GrainInterfaces.csproj
dotnet package add Microsoft.Orleans.Sdk --version 10.2.2 --project Grains/Grains.csproj
dotnet package add Microsoft.Orleans.Server --version 10.2.2 --project Silo/Silo.csproj
dotnet package add Microsoft.Orleans.Client --version 10.2.2 --project Client/Client.csproj
dotnet package add Microsoft.Extensions.Hosting --version 10.0.9 --project Silo/Silo.csproj
dotnet package add Microsoft.Extensions.Hosting --version 10.0.9 --project Client/Client.csproj

The client references only GrainInterfaces. It doesn’t need the grain implementation assembly. The silo references Grains, which in turn references GrainInterfaces.

Delete GrainInterfaces/Class1.cs, create GrainInterfaces/IHello.cs, and add the following grain interface:

public interface IHello : IGrainWithStringKey
{
ValueTask<string> SayHello(string greeting);
}

IGrainWithStringKey identifies the grain by a string key. Grain contracts use asynchronous return types because calls can cross process and network boundaries.

Delete Grains/Class1.cs, create Grains/HelloGrain.cs, and add the following implementation:

public sealed class HelloGrain : Grain, IHello
{
private readonly ILogger<HelloGrain> _logger;
public HelloGrain(ILogger<HelloGrain> logger)
{
_logger = logger;
}
public ValueTask<string> SayHello(string greeting)
{
_logger.LogInformation(
"SayHello message received: greeting = {Greeting}",
greeting);
return ValueTask.FromResult($"Hello, {greeting}!");
}
}

The implementation inherits from Grain and implements IHello. Orleans source generators discover the grain contract and implementation at build time, so you don’t need to register application parts manually.

Replace Silo/Program.cs with the following code:

using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.UseOrleans(siloBuilder =>
{
siloBuilder.UseLocalhostClustering();
});
using var host = builder.Build();
await host.RunAsync();

UseOrleans adds the Orleans silo to the .NET Generic Host. UseLocalhostClustering configures development-only clustering and gateway endpoints on the local machine.

The silo project references Grains, so the runtime can discover and activate HelloGrain.

Replace Client/Program.cs with the following code:

using GrainInterfaces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Orleans;
var builder = Host.CreateApplicationBuilder(args);
builder.UseOrleansClient(clientBuilder =>
{
clientBuilder.UseLocalhostClustering();
});
using var host = builder.Build();
await host.StartAsync();
var grainFactory = host.Services.GetRequiredService<IGrainFactory>();
var friend = grainFactory.GetGrain<IHello>("friend");
var response = await friend.SayHello("Hi friend");
Console.WriteLine(response);
await host.StopAsync();

UseOrleansClient adds an external Orleans client to the Generic Host. The client uses the same localhost clustering configuration as the silo. After the host starts, the client resolves IGrainFactory from dependency injection, obtains a grain reference, and invokes the grain.

Build the solution:

Terminal window
dotnet build OrleansHelloWorld.slnx

Start the silo in the first terminal:

Terminal window
dotnet run --project Silo

Wait until the silo prints Application started, then start the client in the second terminal:

Terminal window
dotnet run --project Client

The client prints the grain response:

Hello, Hi friend!

The client never creates or locates a HelloGrain object directly. GetGrain<IHello>("friend") returns a logical reference. When the client invokes SayHello, Orleans routes the call through a silo gateway and activates the grain if it isn’t already active.

Stop the silo by pressing Ctrl+C in its terminal.