Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
davincidreams avatar

Orleans

  • 10 installs
  • 17 repo stars
  • Updated February 16, 2026
  • davincidreams/agent-team-plugins

Microsoft Orleans patterns for distributed game servers using Grains, Silos, persistence, and multiplayer architecture.

About

Provides Microsoft Orleans patterns for distributed game servers covering Grains, Silos, persistence, and multiplayer architecture. A developer uses it when building scalable multiplayer backends.

  • Grain and Silo distributed actor patterns
  • Persistence and multiplayer game server architecture

Orleans by the numbers

  • 10 all-time installs (skills.sh)
  • Ranked #3,590 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
  • Data as of Jul 30, 2026 (Skillselion catalog sync)
npx skills add https://github.com/davincidreams/agent-team-plugins --skill orleans

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs10
repo stars17
Last updatedFebruary 16, 2026
Repositorydavincidreams/agent-team-plugins

What it does

Microsoft Orleans patterns for distributed game servers using Grains, Silos, persistence, and multiplayer architecture.

Files

SKILL.mdMarkdownGitHub ↗

Microsoft Orleans Game Server Skill

Engine Detection

Look for: .sln with Orleans NuGet packages, *Grain*.cs, *Silo*.cs, Microsoft.Orleans.* in .csproj, ISiloBuilder, IClusterClient

Project Structure

GameServer/
  GameServer.sln
  src/
    GameServer.Grains.Interfaces/    # Grain interfaces (shared)
      IPlayerGrain.cs
      IRoomGrain.cs
      IMatchGrain.cs
      ILeaderboardGrain.cs
    GameServer.Grains/               # Grain implementations
      PlayerGrain.cs
      RoomGrain.cs
      MatchGrain.cs
      LeaderboardGrain.cs
    GameServer.Silo/                 # Silo host configuration
      Program.cs
      SiloConfig.cs
    GameServer.Client/               # Client SDK / API gateway
      Program.cs
      Controllers/
        GameController.cs
    GameServer.Shared/               # Shared types and DTOs
      Models/
        PlayerState.cs
        MatchState.cs
        GameAction.cs
  tests/
    GameServer.Tests/
      PlayerGrainTests.cs
      MatchGrainTests.cs

Grain Pattern (Virtual Actor)

Grains are the core abstraction. Each grain has a unique identity and is single-threaded:

// Interface - GameServer.Grains.Interfaces/IPlayerGrain.cs
public interface IPlayerGrain : IGrainWithStringKey
{
    Task<PlayerState> GetState();
    Task JoinRoom(string roomId);
    Task LeaveRoom();
    Task<bool> TakeDamage(float amount, string attackerId);
    Task UpdatePosition(Vector3 position, Quaternion rotation);
}

// Implementation - GameServer.Grains/PlayerGrain.cs
public class PlayerGrain : Grain, IPlayerGrain
{
    private readonly IPersistentState<PlayerState> _state;
    private readonly ILogger<PlayerGrain> _logger;
    private IDisposable? _heartbeatTimer;

    public PlayerGrain(
        [PersistentState("player", "gameStore")]
        IPersistentState<PlayerState> state,
        ILogger<PlayerGrain> logger)
    {
        _state = state;
        _logger = logger;
    }

    public override async Task OnActivateAsync(CancellationToken ct)
    {
        _logger.LogInformation("Player {Id} activated", this.GetPrimaryKeyString());
        _heartbeatTimer = this.RegisterGrainTimer(
            Heartbeat, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
        await base.OnActivateAsync(ct);
    }

    public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken ct)
    {
        _heartbeatTimer?.Dispose();
        await _state.WriteStateAsync();
        await base.OnDeactivateAsync(reason, ct);
    }

    public Task<PlayerState> GetState() => Task.FromResult(_state.State);

    public async Task JoinRoom(string roomId)
    {
        var room = GrainFactory.GetGrain<IRoomGrain>(roomId);
        await room.AddPlayer(this.GetPrimaryKeyString());
        _state.State.CurrentRoomId = roomId;
        await _state.WriteStateAsync();
    }

    public async Task<bool> TakeDamage(float amount, string attackerId)
    {
        _state.State.Health -= amount;
        if (_state.State.Health <= 0)
        {
            _state.State.Health = 0;
            _state.State.IsAlive = false;
            await _state.WriteStateAsync();

            // Notify the room
            if (_state.State.CurrentRoomId is not null)
            {
                var room = GrainFactory.GetGrain<IRoomGrain>(_state.State.CurrentRoomId);
                await room.OnPlayerDeath(this.GetPrimaryKeyString(), attackerId);
            }
            return true; // Player died
        }
        await _state.WriteStateAsync();
        return false;
    }

    private Task Heartbeat()
    {
        _state.State.LastHeartbeat = DateTime.UtcNow;
        return _state.WriteStateAsync();
    }
}

Room/Match Grain (Game Session)

public interface IRoomGrain : IGrainWithStringKey
{
    Task AddPlayer(string playerId);
    Task RemovePlayer(string playerId);
    Task<RoomState> GetState();
    Task BroadcastAction(GameAction action);
    Task OnPlayerDeath(string playerId, string killerId);
}

public class RoomGrain : Grain, IRoomGrain
{
    private readonly IPersistentState<RoomState> _state;
    private readonly HashSet<string> _activePlayers = new();

    public RoomGrain(
        [PersistentState("room", "gameStore")]
        IPersistentState<RoomState> state)
    {
        _state = state;
    }

    public async Task AddPlayer(string playerId)
    {
        if (_activePlayers.Count >= _state.State.MaxPlayers)
            throw new InvalidOperationException("Room is full");

        _activePlayers.Add(playerId);
        _state.State.PlayerIds = _activePlayers.ToList();
        await _state.WriteStateAsync();

        // Notify all players
        await BroadcastAction(new GameAction
        {
            Type = "player_joined",
            PlayerId = playerId,
            Timestamp = DateTime.UtcNow
        });
    }

    public async Task BroadcastAction(GameAction action)
    {
        var tasks = _activePlayers.Select(async id =>
        {
            var player = GrainFactory.GetGrain<IPlayerGrain>(id);
            // Push via stream or polling
        });
        await Task.WhenAll(tasks);
    }
}

Orleans Streams (Real-Time Updates)

// Producer (in RoomGrain)
public async Task BroadcastGameState()
{
    var streamProvider = this.GetStreamProvider("GameStream");
    var stream = streamProvider.GetStream<GameStateUpdate>(
        StreamId.Create("room", this.GetPrimaryKeyString()));
    await stream.OnNextAsync(new GameStateUpdate
    {
        RoomId = this.GetPrimaryKeyString(),
        Players = _state.State.PlayerIds,
        Timestamp = DateTime.UtcNow
    });
}

// Consumer (in client or another grain)
var stream = streamProvider.GetStream<GameStateUpdate>(
    StreamId.Create("room", roomId));
await stream.SubscribeAsync((update, token) =>
{
    // Handle real-time game state update
    return Task.CompletedTask;
});

Silo Configuration

// Program.cs - Silo Host
var builder = Host.CreateDefaultBuilder(args)
    .UseOrleans((context, siloBuilder) =>
    {
        if (context.HostingEnvironment.IsDevelopment())
        {
            siloBuilder.UseLocalhostClustering();
            siloBuilder.AddMemoryGrainStorage("gameStore");
        }
        else
        {
            siloBuilder.UseAzureStorageClustering(options =>
                options.ConfigureTableServiceClient(connectionString));
            siloBuilder.AddAzureTableGrainStorage("gameStore", options =>
                options.ConfigureTableServiceClient(connectionString));
        }

        siloBuilder.AddMemoryStreams("GameStream");
        siloBuilder.UseDashboard(); // Orleans Dashboard for monitoring
    });

Persistence Patterns

// State class
[GenerateSerializer]
public class PlayerState
{
    [Id(0)] public string PlayerId { get; set; } = "";
    [Id(1)] public float Health { get; set; } = 100f;
    [Id(2)] public bool IsAlive { get; set; } = true;
    [Id(3)] public string? CurrentRoomId { get; set; }
    [Id(4)] public DateTime LastHeartbeat { get; set; }
    [Id(5)] public Dictionary<string, int> Inventory { get; set; } = new();
}

// Use [GenerateSerializer] and [Id(n)] for Orleans serialization
// Write state explicitly after mutations: await _state.WriteStateAsync()
// State is automatically loaded on grain activation

Key Rules

1. Grains are single-threaded - No locks needed, but avoid blocking calls 2. Use Task/async everywhere - Never block with .Result or .Wait() 3. Keep grain state small - Large state = slow activation/persistence 4. Use grain timers, not Task.Delay - Timers are grain-lifecycle aware 5. Dispose timers in OnDeactivateAsync - Prevent leaks 6. Use streams for real-time communication - Not polling 7. Persist state explicitly - Call WriteStateAsync after mutations 8. Use [GenerateSerializer] - Not JSON serialization for grain state 9. Design for grain deactivation - Grains can deactivate at any time 10. Use string or Guid keys - Choose based on your identity model

Common Anti-Patterns

  • Storing client connections in grain state (not serializable)
  • Calling .Result on Tasks inside grains (deadlock risk)
  • Large grain state with full game world (partition into multiple grains)
  • Not handling grain deactivation/reactivation cycles
  • Using static state instead of grain state (lost on silo restart)

Scaling Patterns

  • One grain per player - Player state, inventory, progression
  • One grain per room/match - Game session state, player list
  • One grain per world zone - Spatial partitioning for large worlds
  • Stateless worker grains - For CPU-intensive calculations (pathfinding, matchmaking)
  • Observer pattern - Use IGrainObserver for push notifications to clients

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.