Orleans
Overview
Microsoft Orleans is a framework for building distributed, scalable, stateful applications using the virtual actor model. Grains (virtual actors) are the fundamental units of computation and state, activated on demand and transparently distributed across a cluster of silos. Orleans handles grain placement, lifecycle, state persistence, and messaging, letting developers focus on business logic while achieving horizontal scalability.
NuGet Packages
dotnet add package Microsoft.Orleans.Server
dotnet add package Microsoft.Orleans.Client
dotnet add package Microsoft.Orleans.Sdk
dotnet add package Microsoft.Orleans.Persistence.AdoNet
dotnet add package Microsoft.Orleans.Persistence.AzureStorage
dotnet add package Microsoft.Orleans.Clustering.AzureStorage
dotnet add package Microsoft.Orleans.Streaming
Silo Setup (Co-hosted with ASP.NET Core)
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseOrleans(silo =>
{
if (builder.Environment.IsDevelopment())
{
silo.UseLocalhostClustering()
.AddMemoryGrainStorage("default")
.AddMemoryGrainStorage("urls");
}
else
{
silo.UseAzureStorageClustering(options =>
{
options.TableServiceClient = new TableServiceClient(
builder.Configuration["Azure:StorageConnectionString"]);
})
.AddAzureTableGrainStorage("default", options =>
{
options.TableServiceClient = new TableServiceClient(
builder.Configuration["Azure:StorageConnectionString"]);
});
}
});
var app = builder.Build();
app.MapGet("/shorten/{*path}", async (IGrainFactory grains, HttpRequest request, string path) =>
{
var shortenedRouteSegment = request.Query["short"].ToString();
var grain = grains.GetGrain<IUrlShortenerGrain>(shortenedRouteSegment);
var url = await grain.GetUrlAsync();
return url is not null ? Results.Redirect(url) : Results.NotFound();
});
app.Run();
Grain Interfaces
Grain interfaces define the contract. All grain methods must return Task or Task<T>.
public interface IPlayerGrain : IGrainWithStringKey
{
Task<PlayerState> GetStateAsync();
Task SetNameAsync(string name);
Task AddScoreAsync(int points);
Task<int> GetScoreAsync();
Task JoinGameAsync(Guid gameId);
}
public interface IGameGrain : IGrainWithGuidKey
{
Task<GameState> GetStateAsync();
Task AddPlayerAsync(string playerId);
Task RemovePlayerAsync(string playerId);
Task StartAsync();
Task EndAsync();
Task<List<string>> GetLeaderboardAsync();
}
public interface IUrlShortenerGrain : IGrainWithStringKey
{
Task SetUrlAsync(string fullUrl);
Task<string?> GetUrlAsync();
}
Grain Key Types
| Interface | Key Type | Use Case |
|---|
IGrainWithStringKey | string | Usernames, slugs, natural IDs |
IGrainWithGuidKey | Guid | Auto-generated entity IDs |
IGrainWithIntegerKey | long | Sequential/numeric IDs |
IGrainWithGuidCompoundKey | Guid + string | Composite keys (tenant + entity) |
IGrainWithIntegerCompoundKey | long + string | Composite keys |
Grain Implementation
public class PlayerGrain : Grain, IPlayerGrain
{
private readonly IPersistentState<PlayerState> _state;
private readonly ILogger<PlayerGrain> _logger;
public PlayerGrain(
[PersistentState("player", "default")] IPersistentState<PlayerState> state,
ILogger<PlayerGrain> logger)
{
_state = state;
_logger = logger;
}
public Task<PlayerState> GetStateAsync() => Task.FromResult(_state.State);
public async Task SetNameAsync(string name)
{
_state.State.Name = name;
_state.State.LastUpdated = DateTime.UtcNow;
await _state.WriteStateAsync();
_logger.LogInformation("Player {Key} set name to {Name}",
this.GetPrimaryKeyString(), name);
}
public async Task AddScoreAsync(int points)
{
_state.State.Score += points;
_state.State.GamesPlayed++;
_state.State.LastUpdated = DateTime.UtcNow;
await _state.WriteStateAsync();
}
public Task<int> GetScoreAsync() => Task.FromResult(_state.State.Score);
public async Task JoinGameAsync(Guid gameId)
{
var game = GrainFactory.GetGrain<IGameGrain>(gameId);
game.AddPlayerAsync(.GetPrimaryKeyString());
_state.State.CurrentGameId = gameId;
_state.WriteStateAsync();
}
}
[]
{
[] Name { ; ; } = ;
[] Score { ; ; }
[] GamesPlayed { ; ; }
[] Guid? CurrentGameId { ; ; }
[] DateTime LastUpdated { ; ; }
}
Grain with Multiple State Objects
public class GameGrain : Grain, IGameGrain
{
private readonly IPersistentState<GameState> _state;
private readonly IPersistentState<GameStats> _stats;
public GameGrain(
[PersistentState("game", "default")] IPersistentState<GameState> state,
[PersistentState("stats", "default")] IPersistentState<GameStats> stats)
{
_state = state;
_stats = stats;
}
public Task<GameState> GetStateAsync() => Task.FromResult(_state.State);
public async Task AddPlayerAsync(string playerId)
{
if (_state.State.Status != GameStatus.Waiting)
throw new InvalidOperationException("Game already started");
_state.State.PlayerIds.Add(playerId);
await _state.WriteStateAsync();
}
public async Task StartAsync()
{
_state.State.Status = GameStatus.Active;
_state.State.StartedAt = DateTime.UtcNow;
await _state.WriteStateAsync();
_stats.State.TotalGamesStarted++;
await _stats.WriteStateAsync();
}
public async Task<List<string>> GetLeaderboardAsync()
{
var tasks = _state.State.PlayerIds.Select(async id =>
{
player = GrainFactory.GetGrain<IPlayerGrain>(id);
score = player.GetScoreAsync();
(Id: id, Score: score);
});
results = Task.WhenAll(tasks);
results.OrderByDescending(r => r.Score)
.Select(r => )
.ToList();
}
}
[]
{
[] List<> PlayerIds { ; ; } = ();
[] GameStatus Status { ; ; } = GameStatus.Waiting;
[] DateTime? StartedAt { ; ; }
}
[]
{
[] TotalGamesStarted { ; ; }
}
GameStatus { Waiting, Active, Completed }
Timers and Reminders
Timers are volatile (lost on deactivation); reminders are persistent and survive restarts.
public class SensorGrain : Grain, ISensorGrain, IRemindable
{
private readonly IPersistentState<SensorState> _state;
private IDisposable? _pollingTimer;
public SensorGrain(
[PersistentState("sensor", "default")] IPersistentState<SensorState> state)
{
_state = state;
}
public override async Task OnActivateAsync(CancellationToken ct)
{
_pollingTimer = RegisterTimer(
callback: PollSensorAsync,
state: null,
dueTime: TimeSpan.FromSeconds(10),
period: TimeSpan.FromSeconds(10));
await this.RegisterOrUpdateReminder(
reminderName: "daily-report",
dueTime: TimeSpan.FromHours(1),
period: TimeSpan.FromHours(24));
await base.OnActivateAsync(ct);
}
private async Task PollSensorAsync(object? state)
{
var reading = await ReadSensorValueAsync();
_state.State.LastReading = reading;
_state.State.ReadingCount++;
await _state.WriteStateAsync();
}
Task ()
{
(reminderName == )
{
report = +
+
;
}
}
{
_pollingTimer?.Dispose();
.OnDeactivateAsync(reason, ct);
}
}
Orleans Streams
public class TemperatureMonitorGrain : Grain, ITemperatureMonitorGrain
{
private IAsyncStream<TemperatureReading>? _stream;
public override Task OnActivateAsync(CancellationToken ct)
{
var streamProvider = this.GetStreamProvider("default");
_stream = streamProvider.GetStream<TemperatureReading>(
StreamId.Create("temperature", this.GetPrimaryKeyString()));
return base.OnActivateAsync(ct);
}
public async Task RecordTemperatureAsync(double value)
{
var reading = new TemperatureReading(
this.GetPrimaryKeyString(), value, DateTime.UtcNow);
await _stream!.OnNextAsync(reading);
}
}
public class AlertGrain : Grain, IAlertGrain, IAsyncObserver<TemperatureReading>
{
public override async Task OnActivateAsync(CancellationToken ct)
{
var streamProvider = this.GetStreamProvider("default");
stream = streamProvider.GetStream<TemperatureReading>(
StreamId.Create(, .GetPrimaryKeyString()));
stream.SubscribeAsync();
.OnActivateAsync(ct);
}
{
(reading.Value > )
{
}
Task.CompletedTask;
}
=> Task.CompletedTask;
=> Task.CompletedTask;
}
[]
;
Calling Grains from ASP.NET Core
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseOrleans(silo => silo.UseLocalhostClustering());
var app = builder.Build();
app.MapGet("/players/{id}", async (string id, IGrainFactory grains) =>
{
var grain = grains.GetGrain<IPlayerGrain>(id);
var state = await grain.GetStateAsync();
return Results.Ok(state);
});
app.MapPost("/players/{id}/score", async (string id, ScoreRequest req, IGrainFactory grains) =>
{
var grain = grains.GetGrain<IPlayerGrain>(id);
await grain.AddScoreAsync(req.Points);
return Results.Ok();
});
app.MapPost("/games", async (IGrainFactory grains) =>
{
var gameId = Guid.NewGuid();
var grain = grains.GetGrain<IGameGrain>(gameId);
return Results.Created($"/games/{gameId}", new { gameId });
});
app.Run();
record ScoreRequest(int Points);
Best Practices
- Design grains around natural entity boundaries (one grain per user, per device, per game session) with a single responsibility; avoid "god grains" that manage unrelated state.
- Use
[PersistentState("name", "storage")] constructor injection for grain state rather than inheriting from Grain<TState>, which provides more flexibility and supports multiple state objects per grain.
- Mark all state classes and DTOs with
[GenerateSerializer] and assign explicit [Id(n)] attributes to each serialized property to ensure forward-compatible serialization across deployments.
- Use
GrainFactory.GetGrain<T>(key) inside grains to call other grains rather than injecting them; Orleans handles activation, routing, and lifecycle automatically.
- Use reminders (persistent, survive restarts) for important scheduled operations like daily reports, and timers (volatile, per-activation) for frequent polling that can be recreated on reactivation.
- Avoid blocking calls or
Task.Result inside grain methods because grains are single-threaded; blocking prevents other messages from being processed and can cause deadlocks.
- Use
UseLocalhostClustering() and AddMemoryGrainStorage() only for development; switch to UseAzureStorageClustering() or UseAdoNetClustering() for production multi-silo deployments.
- Co-host Orleans silos with ASP.NET Core using
builder.Host.UseOrleans() to expose grain functionality via HTTP endpoints using IGrainFactory from dependency injection.
- Call
WriteStateAsync() after every state mutation rather than batching writes, because a silo crash between mutations would lose uncommitted changes.
- Use Orleans Streams for event-driven communication between grains rather than direct grain-to-grain calls when the producer should not know about or depend on consumers.