Skip to content

Commit 35c2fce

Browse files
Migrate to Akka.NET Aspire plugin and upgrade to .NET 10 (#275)
* Migrate to Akka.NET Aspire plugin and upgrade to .NET 10 Replace hand-rolled Akka.NET cluster configuration in Aspire (Azure Storage emulator, manual endpoint/env var injection) with the Aaron.Akka.Aspire plugin's AddAkka().WithClustering().WithReference() API backed by Redis discovery. - Upgrade SDK to .NET 10.0.102, TFM to net10.0 - Upgrade Aspire to 13.1.0, Akka.NET to 1.5.60, Hosting/Management to 1.5.59 - Add Aaron.Akka.Aspire.Hosting, Aaron.Akka.Aspire, Aaron.Akka.Discovery.Redis - Replace Azure Storage emulator with Redis container for cluster discovery - Delete AkkaManagementExtensions.cs (fully replaced by plugin) - Implement dual-path networking in AkkaConfiguration.cs: Aspire plugin path (WithAspireClusterBootstrap + WithRedisDiscovery) when Akka:Cluster:Enabled is set, K8s/standalone path (existing ConfigureNetwork) otherwise - K8s deployment path preserved with zero YAML changes * Add project CLAUDE.md with Aspire MCP and skills reference Document Aspire MCP integration and relevant dotnet-skills for Aspire configuration, integration testing, service defaults, and Akka.NET cluster setup.
1 parent 096c73e commit 35c2fce

File tree

11 files changed

+104
-121
lines changed

11 files changed

+104
-121
lines changed

CLAUDE.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# DrawTogether.NET
2+
3+
## Aspire MCP Integration
4+
5+
This project uses the .NET Aspire MCP server for runtime observability. When the AppHost is running (via `dotnet run` in `src/DrawTogether.AppHost` or `aspire run`), use the `mcp__aspire__*` tools to inspect resources, logs, traces, and health status.
6+
7+
### Aspire Skills
8+
9+
When working with Aspire configuration or debugging Aspire-related issues, invoke these dotnet-skills:
10+
11+
- `dotnet-skills:aspire-configuration` — AppHost-to-app config wiring, explicit env var mapping, feature toggles
12+
- `dotnet-skills:aspire-integration-testing` — Integration tests using Aspire's `DistributedApplicationTestingBuilder`
13+
- `dotnet-skills:aspire-service-defaults` — Shared ServiceDefaults project for OpenTelemetry, health checks, resilience
14+
- `dotnet-skills:akka-aspire-configuration` — Akka.NET + Aspire clustering, persistence, and management setup

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project>
22
<PropertyGroup>
3-
<TargetFramework>net9.0</TargetFramework>
3+
<TargetFramework>net10.0</TargetFramework>
44
<Nullable>enable</Nullable>
55
<ImplicitUsings>enable</ImplicitUsings>
66
</PropertyGroup>

Directory.Packages.props

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<Project>
22
<PropertyGroup Label="SharedVersions">
33
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
4-
<AkkaVersion>1.5.53</AkkaVersion>
5-
<AkkaHostingVersion>1.5.53</AkkaHostingVersion>
6-
<AkkaManagementVersion>1.5.52</AkkaManagementVersion>
4+
<AkkaVersion>1.5.60</AkkaVersion>
5+
<AkkaHostingVersion>1.5.59</AkkaHostingVersion>
6+
<AkkaManagementVersion>1.5.59</AkkaManagementVersion>
77
<PetabridgeCmdVersion>1.4.5</PetabridgeCmdVersion>
8-
<AspireVersion>9.5.1</AspireVersion>
8+
<AspireVersion>13.1.0</AspireVersion>
99
<PlaywrightVersion>1.52.0</PlaywrightVersion>
10-
<MsftVersion>9.0.9</MsftVersion>
10+
<MsftVersion>10.0.2</MsftVersion>
1111
</PropertyGroup>
1212
<!-- Akka.NET Package Versions -->
1313
<ItemGroup>
@@ -17,12 +17,11 @@
1717
<PackageVersion Include="Akka.Discovery.KubernetesApi" Version="$(AkkaManagementVersion)" />
1818
<PackageVersion Include="Akka.Hosting" Version="$(AkkaHostingVersion)" />
1919
<PackageVersion Include="Akka.Management" Version="$(AkkaManagementVersion)" />
20-
<PackageVersion Include="Akka.Persistence.Sql.Hosting" Version="1.5.53" />
20+
<PackageVersion Include="Akka.Persistence.Sql.Hosting" Version="1.5.59" />
2121
<PackageVersion Include="Akka.Streams" Version="$(AkkaVersion)" />
2222
<PackageVersion Include="Akka.Streams.TestKit" Version="$(AkkaVersion)" />
23-
<PackageVersion Include="Aspire.Hosting.Azure.Storage" Version="$(AspireVersion)" />
24-
<PackageVersion Include="Aspire.Hosting.Docker" Version="9.5.1-preview.1.25502.11" />
25-
<PackageVersion Include="Aspire.Hosting.Kubernetes" Version="9.5.1-preview.1.25502.11" />
23+
<PackageVersion Include="Aspire.Hosting.Docker" Version="13.1.1-preview.1.26105.8" />
24+
<PackageVersion Include="Aspire.Hosting.Kubernetes" Version="13.1.1-preview.1.26105.8" />
2625
<PackageVersion Include="Grpc.Tools" Version="2.72.0">
2726
<PrivateAssets>all</PrivateAssets>
2827
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -32,6 +31,13 @@
3231
<PackageVersion Include="Petabridge.Cmd.Cluster" Version="$(PetabridgeCmdVersion)" />
3332
<PackageVersion Include="Petabridge.Cmd.Cluster.Sharding" Version="$(PetabridgeCmdVersion)" />
3433
</ItemGroup>
34+
<!-- Aaron.Akka.Aspire Plugin Packages -->
35+
<ItemGroup>
36+
<PackageVersion Include="Aaron.Akka.Aspire.Hosting" Version="0.1.0" />
37+
<PackageVersion Include="Aaron.Akka.Aspire" Version="0.1.0" />
38+
<PackageVersion Include="Aaron.Akka.Discovery.Redis" Version="0.1.0" />
39+
<PackageVersion Include="Aspire.Hosting.Redis" Version="$(AspireVersion)" />
40+
</ItemGroup>
3541
<!-- ASP.NET Package Versions -->
3642
<ItemGroup>
3743
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="$(MsftVersion)" />
@@ -57,24 +63,24 @@
5763
</ItemGroup>
5864
<!-- OTEL Package Versions -->
5965
<ItemGroup>
60-
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
61-
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
62-
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
63-
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
64-
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
66+
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.0" />
67+
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.15.0" />
68+
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.0" />
69+
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
70+
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.0" />
6571
</ItemGroup>
6672
<!-- Utility Package Versions -->
6773
<ItemGroup>
68-
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
74+
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.102" />
6975
</ItemGroup>
7076
<!-- Test Package Versions -->
7177
<ItemGroup>
7278
<PackageVersion Include="Aspire.Hosting.Testing" Version="$(AspireVersion)" />
7379
<PackageVersion Include="Microsoft.Playwright" Version="$(PlaywrightVersion)" />
7480
<PackageVersion Include="Microsoft.Playwright.MSTest" Version="$(PlaywrightVersion)" />
75-
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
81+
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
7682
<PackageVersion Include="xunit" Version="2.9.3" />
77-
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.0">
83+
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5">
7884
<PrivateAssets>all</PrivateAssets>
7985
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
8086
</PackageVersion>
@@ -84,4 +90,4 @@
8490
<PackageVersion Include="Verify.DiffPlex" Version="1.3.0" />
8591
<PackageVersion Include="Akka.Hosting.TestKit" Version="$(AkkaHostingVersion)" />
8692
</ItemGroup>
87-
</Project>
93+
</Project>

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
33
"rollForward": "latestMinor",
4-
"version": "9.0.302"
4+
"version": "10.0.102"
55
}
66
}

nuget.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<packageSourceMapping>
1010
<packageSource key="nuget">
1111
<package pattern="*" />
12+
<package pattern="Aaron.*" />
1213
</packageSource>
1314
</packageSourceMapping>
1415
</configuration>

src/DrawTogether.AppHost/AkkaManagementExtensions.cs

Lines changed: 0 additions & 79 deletions
This file was deleted.

src/DrawTogether.AppHost/DrawTogether.AppHost.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
</PropertyGroup>
1010

1111
<ItemGroup>
12+
<PackageReference Include="Aaron.Akka.Aspire.Hosting" />
1213
<PackageReference Include="Aspire.Hosting.AppHost" />
13-
<PackageReference Include="Aspire.Hosting.Azure.Storage" />
1414
<PackageReference Include="Aspire.Hosting.Docker" />
1515
<PackageReference Include="Aspire.Hosting.Kubernetes" />
16+
<PackageReference Include="Aspire.Hosting.Redis" />
1617
<PackageReference Include="Aspire.Hosting.SqlServer" />
1718
</ItemGroup>
1819

src/DrawTogether.AppHost/Program.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Aaron.Akka.Aspire.Hosting;
12
using DrawTogether.AppHost;
23
using Microsoft.Extensions.Configuration;
34

@@ -8,7 +9,6 @@
89

910
// Adding a default password for ease of use - we can get rid of this but for a quick "git clone and run" it makes sense
1011
// have to add this when using data volumes otherwise Aspire will brick itself
11-
1212
var saPassword = builder.AddParameter(
1313
"sql-sa-password",
1414
() => "YourStrong!Passw0rd", // *must* satisfy SQL Server complexity rules
@@ -31,8 +31,18 @@
3131
var drawTogether = builder.AddProject<Projects.DrawTogether>("DrawTogether")
3232
.WithReplicas(drawTogetherAspireConfig.Replicas)
3333
.WithReference(db, "DefaultConnection")
34-
.WaitForCompletion(migrationService)
35-
.ConfigureAkkaManagementForApp(drawTogetherAspireConfig);
34+
.WaitForCompletion(migrationService);
35+
36+
if (drawTogetherAspireConfig.UseAkkaManagement)
37+
{
38+
var redis = builder.AddRedis("akka-discovery");
39+
var akka = builder.AddAkka("drawtogether").WithClustering(redis);
40+
drawTogether.WithReference(akka);
41+
}
42+
43+
// PBM port still needs explicit endpoint since plugin doesn't handle it
44+
drawTogether.WithEndpoint(name: "pbm", protocol: System.Net.Sockets.ProtocolType.Tcp,
45+
env: "AkkaSettings__PbmOptions__Port");
3646

3747
// https://github.com/petabridge/pbm-sidecar - used to run `pbm` commands on the DrawTogether actor system
3848
var pbmSidecar = builder.AddContainer("pbm-sidecar", "petabridge/pbm:latest")

src/DrawTogether/Config/AkkaConfiguration.cs

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Diagnostics;
1+
using Aaron.Akka.Aspire;
2+
using Aaron.Akka.Discovery.Redis;
23
using Akka.Cluster.Hosting;
34
using Akka.Discovery.Azure;
45
using Akka.Discovery.Config.Hosting;
@@ -19,22 +20,42 @@ namespace DrawTogether.Config;
1920

2021
public static class AkkaConfiguration
2122
{
22-
public static IServiceCollection ConfigureAkka(this IServiceCollection services, IConfiguration configuration, Action<AkkaConfigurationBuilder, IServiceProvider> additionalConfig)
23+
public static IServiceCollection ConfigureAkka(this IServiceCollection services, IConfiguration configuration,
24+
Action<AkkaConfigurationBuilder, IServiceProvider> additionalConfig)
2325
{
2426
var akkaSettings = BindAkkaSettings(services, configuration);
2527

2628
var connectionString = configuration.GetConnectionString("DefaultConnection");
2729
if (connectionString is null)
2830
throw new Exception("DefaultConnection ConnectionString is missing");
29-
31+
3032
const string roleName = ClusterConstants.DrawStateRoleName;
3133

3234
services.AddAkka(akkaSettings.ActorSystemName, (builder, provider) =>
3335
{
34-
builder.ConfigureNetwork(provider)
36+
var config = provider.GetRequiredService<IConfiguration>();
37+
var aspireEnabled = config.GetValue<bool>("Akka:Cluster:Enabled");
38+
39+
if (aspireEnabled)
40+
{
41+
// ASPIRE PATH — plugin handles remote, cluster, management, bootstrap, discovery
42+
builder.WithAspireClusterBootstrap(provider,
43+
configureDiscovery: (b, cfg) =>
44+
{
45+
var redisConn = cfg.GetConnectionString("akka-discovery");
46+
if (!string.IsNullOrEmpty(redisConn))
47+
b.WithRedisDiscovery(redisConn, cfg["Akka:Cluster:ServiceName"]);
48+
},
49+
clusterConfigure: c => c.Roles = [roleName]);
50+
}
51+
else
52+
{
53+
// KUBERNETES / STANDALONE PATH — existing manual configuration
54+
builder.ConfigureNetwork(provider);
55+
}
56+
57+
builder
3558
.AddDrawingProtocolSerializer()
36-
.WithAkkaClusterReadinessCheck()
37-
.WithActorSystemLivenessCheck()
3859
.WithSqlPersistence(
3960
connectionString: connectionString,
4061
providerName: ProviderName.SqlServer2022,
@@ -44,18 +65,18 @@ public static IServiceCollection ConfigureAkka(this IServiceCollection services,
4465
useWriterUuidColumn: true,
4566
autoInitialize: true, journalBuilder: journalBuilder =>
4667
{
47-
journalBuilder.WithHealthCheck(name:"Akka.Persistence.Sql.Journal[default]");
68+
journalBuilder.WithHealthCheck(name: "Akka.Persistence.Sql.Journal[default]");
4869
}, snapshotBuilder: snapshotBuilder =>
4970
{
50-
snapshotBuilder.WithHealthCheck(name:"Akka.Persistence.Sql.SnapshotStore[default]");
71+
snapshotBuilder.WithHealthCheck(name: "Akka.Persistence.Sql.SnapshotStore[default]");
5172
})
5273
.AddAllDrawingsIndexActor(roleName)
5374
.AddDrawingSessionActor(roleName)
5475
.AddLocalDrawingSessionActor();
55-
76+
5677
additionalConfig(builder, provider);
5778
});
58-
79+
5980
return services;
6081
}
6182

@@ -102,11 +123,13 @@ public static AkkaConfigurationBuilder ConfigureNetwork(this AkkaConfigurationBu
102123
{
103124
options.ContactPointDiscovery.ServiceName = settings.AkkaManagementOptions.ServiceName;
104125
options.ContactPointDiscovery.PortName = settings.AkkaManagementOptions.PortName;
105-
options.ContactPointDiscovery.RequiredContactPointsNr = settings.AkkaManagementOptions.RequiredContactPointsNr;
126+
options.ContactPointDiscovery.RequiredContactPointsNr =
127+
settings.AkkaManagementOptions.RequiredContactPointsNr;
106128
options.ContactPointDiscovery.ContactWithAllContactPoints = true;
107129
options.ContactPointDiscovery.StableMargin = TimeSpan.FromSeconds(5);
108-
109-
options.ContactPoint.FilterOnFallbackPort = settings.AkkaManagementOptions.FilterOnFallbackPort;
130+
131+
options.ContactPoint.FilterOnFallbackPort =
132+
settings.AkkaManagementOptions.FilterOnFallbackPort;
110133
}, autoStart: true);
111134

112135
switch (settings.AkkaManagementOptions.DiscoveryMethod)
@@ -122,8 +145,9 @@ public static AkkaConfigurationBuilder ConfigureNetwork(this AkkaConfigurationBu
122145
{
123146
var connectionString = configuration.GetConnectionString("AkkaManagementAzure");
124147
if (connectionString is null)
125-
throw new Exception("AkkaManagement table storage connection string [AkkaManagementAzure] is missing");
126-
148+
throw new Exception(
149+
"AkkaManagement table storage connection string [AkkaManagementAzure] is missing");
150+
127151
builder
128152
.WithAzureDiscovery(options =>
129153
{
@@ -154,6 +178,11 @@ public static AkkaConfigurationBuilder ConfigureNetwork(this AkkaConfigurationBu
154178
default:
155179
throw new ArgumentOutOfRangeException();
156180
}
181+
182+
// Health checks for K8s path only — Aspire plugin adds its own
183+
builder
184+
.WithAkkaClusterReadinessCheck()
185+
.WithActorSystemLivenessCheck();
157186
}
158187
else
159188
{
@@ -189,4 +218,4 @@ private static void ConfigureRemoteOptionsWithTls(AkkaSettings settings)
189218
.ToArray();
190219
}
191220
}
192-
}
221+
}

src/DrawTogether/DrawTogether.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
</PropertyGroup>
3131

3232
<ItemGroup>
33+
<PackageReference Include="Aaron.Akka.Aspire" />
34+
<PackageReference Include="Aaron.Akka.Discovery.Redis" />
3335
<PackageReference Include="Akka.Discovery.Azure" />
3436
<PackageReference Include="Akka.Discovery.KubernetesApi" />
3537
<PackageReference Include="Akka.Management" />

0 commit comments

Comments
 (0)