Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<Sdk Name="Aspire.AppHost.Sdk" Version="13.2.4" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" />
<PackageReference Include="Aspire.Hosting.Orleans" />
<PackageReference Include="Aspire.Hosting.Redis" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\BasicClustering.Silo\BasicClustering.Silo.csproj" />
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions samples/BasicClustering/BasicClustering.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
var builder = DistributedApplication.CreateBuilder(args);

var redis = builder.AddRedis("clustering");
var orleans = builder.AddOrleans("default")
.WithClustering(redis);

builder.AddProject<Projects.BasicClustering_Silo>("silo")
.WithReference(orleans)
.WaitFor(redis)
.WithReplicas(2);

builder.Build().Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15347",
"environmentVariables": {
"ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true",
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19347",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20347"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.StackExchange.Redis" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Orleans.Clustering.Redis" />
<PackageReference Include="Microsoft.Orleans.Server" />
</ItemGroup>

</Project>
54 changes: 54 additions & 0 deletions samples/BasicClustering/BasicClustering.Silo/ClusterMonitor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Orleans.Runtime;

namespace BasicClustering;

public sealed partial class ClusterMonitor(
IClusterMembershipService membership,
IGrainFactory grainFactory,
ILogger<ClusterMonitor> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var grainCalled = false;

await foreach (var snapshot in membership.MembershipUpdates.WithCancellation(stoppingToken))
{
var activeMembers = snapshot.Members.Values
.Where(static member => member.Status is SiloStatus.Active)
.OrderBy(static member => member.Name)
.ToArray();

var memberList = string.Join(
", ",
activeMembers.Select(static member => $"{member.Name} at {member.SiloAddress}"));
LogClusterView(logger, activeMembers.Length, memberList);

if (activeMembers.Length < 2 || grainCalled)
{
continue;
}

var grain = grainFactory.GetGrain<IHelloGrain>(0);
var response = await grain.SayHello("Hello from the cluster");
LogGrainResponse(logger, response);
grainCalled = true;
}
}

[LoggerMessage(
EventId = 1,
Level = LogLevel.Information,
Message = "Observed {ActiveSiloCount} active silo(s): {ActiveSilos}")]
private static partial void LogClusterView(
ILogger logger,
int activeSiloCount,
string activeSilos);

[LoggerMessage(
EventId = 2,
Level = LogLevel.Information,
Message = "The two-silo cluster is ready. {GrainResponse}")]
private static partial void LogGrainResponse(ILogger logger, string grainResponse);
}
7 changes: 7 additions & 0 deletions samples/BasicClustering/BasicClustering.Silo/HelloGrain.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace BasicClustering;

public sealed class HelloGrain : Grain, IHelloGrain
{
public Task<string> SayHello(string greeting) =>
Task.FromResult($"{greeting}. Grain {this.GetPrimaryKeyLong()} is running on {RuntimeIdentity}.");
}
6 changes: 6 additions & 0 deletions samples/BasicClustering/BasicClustering.Silo/IHelloGrain.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace BasicClustering;

public interface IHelloGrain : IGrainWithIntegerKey
{
Task<string> SayHello(string greeting);
}
11 changes: 11 additions & 0 deletions samples/BasicClustering/BasicClustering.Silo/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using BasicClustering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);

builder.AddKeyedRedisClient("clustering");
builder.UseOrleans();
builder.Services.AddHostedService<ClusterMonitor>();

await builder.Build().RunAsync();
4 changes: 4 additions & 0 deletions samples/BasicClustering/BasicClustering.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<Solution>
<Project Path="BasicClustering.AppHost/BasicClustering.AppHost.csproj" />
<Project Path="BasicClustering.Silo/BasicClustering.Silo.csproj" />
</Solution>
17 changes: 17 additions & 0 deletions samples/BasicClustering/Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>

<ItemGroup>
<PackageVersion Include="Aspire.Hosting.AppHost" Version="13.2.4" />
<PackageVersion Include="Aspire.Hosting.Orleans" Version="13.2.4" />
<PackageVersion Include="Aspire.Hosting.Redis" Version="13.2.4" />
<PackageVersion Include="Aspire.StackExchange.Redis" Version="13.2.4" />
<PackageVersion Include="MessagePack" Version="3.1.7" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
<PackageVersion Include="Microsoft.Orleans.Clustering.Redis" Version="10.2.2" />
<PackageVersion Include="Microsoft.Orleans.Server" Version="10.2.2" />
</ItemGroup>
</Project>
47 changes: 47 additions & 0 deletions samples/BasicClustering/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Basic Clustering

This sample starts two Orleans silos, joins them through Redis cluster membership,
and calls a grain after both silos are active. It focuses only on the step between
running Orleans in one process and deploying a production cluster.

## What the sample demonstrates

- `Aspire.Hosting.Orleans` supplies each replica with a unique silo endpoint and
shared cluster identity.
- Redis provides cluster membership and gateway discovery.
- `.WithReplicas(2)` starts two instances of the same silo project.
- Orleans routes both callers to the same grain activation, regardless of which
silo hosts it.

`UseLocalhostClustering()` and `UseDevelopmentClustering()` are the modern
in-memory equivalents of the former `MembershipTableGrain` approach. They are
development-only and require manually coordinating endpoints when multiple
processes run on one machine. This sample instead uses the same external
membership-provider model as a production cluster while Aspire supplies a local
Redis container and process-specific endpoints.

## Run the sample

Install the .NET 10 SDK, the Aspire CLI, and a Docker-compatible container
runtime. From this directory, run:

```powershell
aspire run --apphost BasicClustering.AppHost/BasicClustering.AppHost.csproj
```

In the Aspire dashboard, open the structured logs for either `silo` replica.
After both replicas join, each reports two active silos and a response like:

```text
The two-silo cluster is ready. Hello from the cluster. Grain 0 is running on S10.0.0.1:11111:...
```

Stop either replica in the dashboard to see the remaining silo observe the
membership change. Restart it to return to a two-silo cluster.

## Production guidance

The Redis container created by the AppHost is for local development. In
production, configure a secured, highly available managed Redis service or
another supported clustering provider, keep cluster and service IDs stable
within an environment, and run silo replicas across failure domains.
1 change: 1 addition & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ The command checks the gallery manifest and builds every project in `Samples.sln
| [Adventure](Adventure) | A text adventure game demonstrating grains, external clients, and application modeling. | C# | games, clients, grains | [dotnet/samples](https://github.com/dotnet/samples) |
| [AWS Kinesis and DynamoDB](AWS/KinesisDynamoDB) | An AWS-hosted Orleans application using DynamoDB for clustering, persistence, reminders, and Kinesis checkpoints. | C# | aws, kinesis, dynamodb, streaming | [dotnet/orleans](https://github.com/dotnet/orleans) |
| [Bank Account](BankAccount) | A bank transfer simulation demonstrating ACID transactions across stateful grains. | C# | transactions, persistence | [dotnet/samples](https://github.com/dotnet/samples) |
| [Basic Clustering](BasicClustering) | A minimal Aspire-hosted Orleans cluster with two silo replicas and Redis membership. | C# | clustering, aspire, redis, getting-started | [dotnet/orleans](https://github.com/dotnet/orleans) |
| [Blazor Server](Blazor/BlazorServer) | An interactive Blazor Server application backed by Orleans grains. | C#, Razor | blazor, aspnet-core, web | [dotnet/samples](https://github.com/dotnet/samples) |
| [Blazor WebAssembly](Blazor/BlazorWasm) | A hosted Blazor WebAssembly application with an Orleans-backed server. | C#, Razor | blazor, webassembly, web | [dotnet/samples](https://github.com/dotnet/samples) |
| [Chat Room](ChatRoom) | A terminal chat application demonstrating Orleans Streams. | C# | streaming, client, terminal | [dotnet/samples](https://github.com/dotnet/samples) |
Expand Down
4 changes: 4 additions & 0 deletions samples/Samples.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
<Project Path="BankAccount/BankClient/BankClient.csproj" />
<Project Path="BankAccount/BankServer/BankServer.csproj" />
</Folder>
<Folder Name="/BasicClustering/">
<Project Path="BasicClustering/BasicClustering.AppHost/BasicClustering.AppHost.csproj" />
<Project Path="BasicClustering/BasicClustering.Silo/BasicClustering.Silo.csproj" />
</Folder>
<Folder Name="/Blazor/">
<Project Path="Blazor/BlazorServer/BlazorServer.csproj" />
</Folder>
Expand Down
11 changes: 11 additions & 0 deletions samples/gallery.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@
"tags": ["transactions", "persistence"],
"featured": false
},
{
"slug": "basic-clustering",
"title": "Basic Clustering",
"description": "A minimal Aspire-hosted Orleans cluster with two silo replicas and Redis membership.",
"path": "BasicClustering",
"sourceRepository": "https://github.com/dotnet/orleans",
"image": null,
"languages": ["C#"],
"tags": ["clustering", "aspire", "redis", "getting-started"],
"featured": false
},
{
"slug": "blazor-server",
"title": "Blazor Server",
Expand Down