akka-net-aspire-configuration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseConfiguring Akka.NET with .NET Aspire
为.NET Aspire配置Akka.NET
When to Use This Skill
何时使用此技能
Use this skill when:
- Setting up a new Akka.NET project with .NET Aspire orchestration
- Configuring Akka.Cluster with cluster bootstrapping and discovery
- Integrating Akka.Persistence with SQL Server
- Setting up Akka.Management for cluster management
- Configuring multi-replica actor systems in local development
- Deploying Akka.NET applications to Kubernetes with Aspire
在以下场景中使用此技能:
- 借助.NET Aspire编排搭建新的Akka.NET项目
- 配置Akka.Cluster并实现集群引导与服务发现
- 集成Akka.Persistence与SQL Server
- 搭建Akka.Management以实现集群管理
- 在本地开发环境中配置多副本Actor系统
- 借助Aspire将Akka.NET应用部署至Kubernetes
Related Skills
相关技能
- - Deep dive into Akka.Management, Cluster Bootstrap, and discovery providers (Kubernetes, Azure, Config)
akka-net-management - - IValidateOptions patterns for configuration validation
microsoft-extensions-configuration - - Cluster/local mode abstractions for testable actor systems
akka-net-best-practices - - Testing Aspire applications with real infrastructure
aspire-integration-testing
- - 深入讲解Akka.Management、Cluster Bootstrap以及服务发现提供方(Kubernetes、Azure、配置文件)
akka-net-management - - 用于配置验证的IValidateOptions模式
microsoft-extensions-configuration - - 可测试Actor系统的集群/本地模式抽象
akka-net-best-practices - - 使用真实基础设施测试Aspire应用
aspire-integration-testing
Core Principles
核心原则
- Configuration via Microsoft.Extensions.Configuration - Use strongly-typed settings classes bound from appsettings.json (see skill)
microsoft-extensions-configuration - Akka.Hosting for DI Integration - Use the Akka.Hosting library for seamless ASP.NET Core integration
- Aspire for Orchestration - Let Aspire manage service dependencies, networking, and environment configuration
- Health Checks - Always configure health checks for clustering, persistence, and readiness
- Separate Concerns - Keep actor definitions, configuration, and Aspire orchestration in separate layers
- Validate Configuration at Startup - Use and
IValidateOptions<T>to fail fast on misconfiguration.ValidateOnStart()
- 通过Microsoft.Extensions.Configuration配置 - 使用从appsettings.json绑定的强类型设置类(详见技能)
microsoft-extensions-configuration - Akka.Hosting实现DI集成 - 使用Akka.Hosting库实现与ASP.NET Core的无缝集成
- Aspire负责编排 - 让Aspire管理服务依赖、网络与环境配置
- 健康检查 - 始终为集群、持久化与就绪状态配置健康检查
- 关注点分离 - 将Actor定义、配置与Aspire编排置于不同层级
- 启动时验证配置 - 使用和
IValidateOptions<T>在配置错误时快速失败.ValidateOnStart()
Project Structure
项目结构
YourSolution/
├── src/
│ ├── YourApp.Actors/ # Actor definitions and business logic
│ │ ├── YourActor.cs
│ │ └── YourApp.Actors.csproj
│ ├── YourApp/ # ASP.NET Core web application
│ │ ├── Config/
│ │ │ ├── AkkaConfiguration.cs # Akka setup extension methods
│ │ │ └── AkkaSettings.cs # Configuration model
│ │ ├── Program.cs
│ │ ├── appsettings.json
│ │ └── YourApp.csproj
│ └── YourApp.AppHost/ # Aspire orchestration
│ ├── Program.cs
│ ├── AkkaManagementExtensions.cs
│ └── YourApp.AppHost.csprojYourSolution/
├── src/
│ ├── YourApp.Actors/ # Actor定义与业务逻辑
│ │ ├── YourActor.cs
│ │ └── YourApp.Actors.csproj
│ ├── YourApp/ # ASP.NET Core Web应用
│ │ ├── Config/
│ │ │ ├── AkkaConfiguration.cs # Akka设置扩展方法
│ │ │ └── AkkaSettings.cs # 配置模型
│ │ ├── Program.cs
│ │ ├── appsettings.json
│ │ └── YourApp.csproj
│ └── YourApp.AppHost/ # Aspire编排
│ ├── Program.cs
│ ├── AkkaManagementExtensions.cs
│ └── YourApp.AppHost.csprojRequired NuGet Packages
所需NuGet包
For Actor Project (YourApp.Actors.csproj)
适用于Actor项目(YourApp.Actors.csproj)
xml
<ItemGroup>
<PackageReference Include="Akka.Cluster.Hosting" />
<PackageReference Include="Akka.Streams" />
</ItemGroup>xml
<ItemGroup>
<PackageReference Include="Akka.Cluster.Hosting" />
<PackageReference Include="Akka.Streams" />
</ItemGroup>For Web Application (YourApp.csproj)
适用于Web应用(YourApp.csproj)
xml
<ItemGroup>
<PackageReference Include="Akka.Hosting" />
<PackageReference Include="Akka.Cluster.Hosting" />
<PackageReference Include="Akka.Persistence.Sql.Hosting" />
<PackageReference Include="Akka.Management" />
<PackageReference Include="Akka.Management.Cluster.Bootstrap" />
<PackageReference Include="Akka.Discovery.KubernetesApi" />
<PackageReference Include="Akka.Discovery.Azure" />
<PackageReference Include="Akka.Discovery.Config.Hosting" />
<PackageReference Include="Petabridge.Cmd.Host" />
<PackageReference Include="Petabridge.Cmd.Cluster" />
</ItemGroup>xml
<ItemGroup>
<PackageReference Include="Akka.Hosting" />
<PackageReference Include="Akka.Cluster.Hosting" />
<PackageReference Include="Akka.Persistence.Sql.Hosting" />
<PackageReference Include="Akka.Management" />
<PackageReference Include="Akka.Management.Cluster.Bootstrap" />
<PackageReference Include="Akka.Discovery.KubernetesApi" />
<PackageReference Include="Akka.Discovery.Azure" />
<PackageReference Include="Akka.Discovery.Config.Hosting" />
<PackageReference Include="Petabridge.Cmd.Host" />
<PackageReference Include="Petabridge.Cmd.Cluster" />
</ItemGroup>For AppHost (YourApp.AppHost.csproj)
适用于AppHost(YourApp.AppHost.csproj)
xml
<Sdk Name="Aspire.AppHost.Sdk" Version="$(AspireVersion)" />
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" />
<PackageReference Include="Aspire.Hosting.Azure.Storage" />
<PackageReference Include="Aspire.Hosting.SqlServer" />
</ItemGroup>xml
<Sdk Name="Aspire.AppHost.Sdk" Version="$(AspireVersion)" />
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" />
<PackageReference Include="Aspire.Hosting.Azure.Storage" />
<PackageReference Include="Aspire.Hosting.SqlServer" />
</ItemGroup>Configuration Model (AkkaSettings.cs)
配置模型(AkkaSettings.cs)
Create a strongly-typed configuration class:
csharp
using System.Net;
using System.Security.Cryptography.X509Certificates;
using Akka.Cluster.Hosting;
using Akka.Remote.Hosting;
using Petabridge.Cmd.Host;
namespace YourApp.Config;
public class AkkaSettings
{
public string ActorSystemName { get; set; } = "YourSystem";
public bool LogConfigOnStart { get; set; } = false;
public RemoteOptions RemoteOptions { get; set; } = new()
{
PublicHostName = Dns.GetHostName(),
HostName = "0.0.0.0",
Port = 8081
};
public ClusterOptions ClusterOptions { get; set; } = new()
{
SeedNodes = [$"akka.tcp://YourSystem@{Dns.GetHostName()}:8081"],
Roles = ["your-role"]
};
public ShardOptions ShardOptions { get; set; } = new();
public AkkaManagementOptions? AkkaManagementOptions { get; set; }
public PetabridgeCmdOptions PbmOptions { get; set; } = new()
{
Host = "0.0.0.0",
Port = 9110
};
public TlsSettings? TlsSettings { get; set; }
}
public class TlsSettings
{
public bool Enabled { get; set; } = false;
public string? CertificatePath { get; set; }
public string? CertificatePassword { get; set; }
public bool ValidateCertificates { get; set; } = true;
public X509Certificate2? LoadCertificate()
{
if (string.IsNullOrWhiteSpace(CertificatePath))
return null;
if (!File.Exists(CertificatePath))
throw new FileNotFoundException($"Certificate file not found at: {CertificatePath}");
return !string.IsNullOrWhiteSpace(CertificatePassword)
? X509CertificateLoader.LoadPkcs12FromFile(CertificatePath, CertificatePassword)
: X509CertificateLoader.LoadCertificateFromFile(CertificatePath);
}
}
public class AkkaManagementOptions
{
public bool Enabled { get; set; }
public string? Hostname { get; set; }
public int Port { get; set; } = 8558;
public string ServiceName { get; set; } = "your-service";
public string PortName { get; set; } = "management";
public int RequiredContactPointsNr { get; set; } = 1;
public bool FilterOnFallbackPort { get; set; } = true;
public DiscoveryMethod DiscoveryMethod { get; set; } = DiscoveryMethod.Config;
}
public enum DiscoveryMethod
{
Config,
Kubernetes,
AzureTableStorage,
AwsEcsTagBased,
AwsEc2TagBased
}创建强类型配置类:
csharp
using System.Net;
using System.Security.Cryptography.X509Certificates;
using Akka.Cluster.Hosting;
using Akka.Remote.Hosting;
using Petabridge.Cmd.Host;
namespace YourApp.Config;
public class AkkaSettings
{
public string ActorSystemName { get; set; } = "YourSystem";
public bool LogConfigOnStart { get; set; } = false;
public RemoteOptions RemoteOptions { get; set; } = new()
{
PublicHostName = Dns.GetHostName(),
HostName = "0.0.0.0",
Port = 8081
};
public ClusterOptions ClusterOptions { get; set; } = new()
{
SeedNodes = [$"akka.tcp://YourSystem@{Dns.GetHostName()}:8081"],
Roles = ["your-role"]
};
public ShardOptions ShardOptions { get; set; } = new();
public AkkaManagementOptions? AkkaManagementOptions { get; set; }
public PetabridgeCmdOptions PbmOptions { get; set; } = new()
{
Host = "0.0.0.0",
Port = 9110
};
public TlsSettings? TlsSettings { get; set; }
}
public class TlsSettings
{
public bool Enabled { get; set; } = false;
public string? CertificatePath { get; set; }
public string? CertificatePassword { get; set; }
public bool ValidateCertificates { get; set; } = true;
public X509Certificate2? LoadCertificate()
{
if (string.IsNullOrWhiteSpace(CertificatePath))
return null;
if (!File.Exists(CertificatePath))
throw new FileNotFoundException($"Certificate file not found at: {CertificatePath}");
return !string.IsNullOrWhiteSpace(CertificatePassword)
? X509CertificateLoader.LoadPkcs12FromFile(CertificatePath, CertificatePassword)
: X509CertificateLoader.LoadCertificateFromFile(CertificatePath);
}
}
public class AkkaManagementOptions
{
public bool Enabled { get; set; }
public string? Hostname { get; set; }
public int Port { get; set; } = 8558;
public string ServiceName { get; set; } = "your-service";
public string PortName { get; set; } = "management";
public int RequiredContactPointsNr { get; set; } = 1;
public bool FilterOnFallbackPort { get; set; } = true;
public DiscoveryMethod DiscoveryMethod { get; set; } = DiscoveryMethod.Config;
}
public enum DiscoveryMethod
{
Config,
Kubernetes,
AzureTableStorage,
AwsEcsTagBased,
AwsEc2TagBased
}Akka Configuration Extension Methods (AkkaConfiguration.cs)
Akka配置扩展方法(AkkaConfiguration.cs)
csharp
using Akka.Cluster.Hosting;
using Akka.Discovery.Azure;
using Akka.Discovery.Config.Hosting;
using Akka.Discovery.KubernetesApi;
using Akka.Hosting;
using Akka.Management;
using Akka.Management.Cluster.Bootstrap;
using Akka.Persistence.Sql.Config;
using Akka.Persistence.Sql.Hosting;
using Akka.Remote.Hosting;
using LinqToDB;
namespace YourApp.Config;
public static class AkkaConfiguration
{
public static IServiceCollection ConfigureAkka(
this IServiceCollection services,
IConfiguration configuration,
Action<AkkaConfigurationBuilder, IServiceProvider> additionalConfig)
{
var akkaSettings = BindAkkaSettings(services, configuration);
var connectionString = configuration.GetConnectionString("DefaultConnection");
if (connectionString is null)
throw new Exception("DefaultConnection ConnectionString is missing");
const string roleName = "your-role";
services.AddAkka(akkaSettings.ActorSystemName, (builder, provider) =>
{
builder.ConfigureNetwork(provider)
.WithAkkaClusterReadinessCheck()
.WithActorSystemLivenessCheck()
.WithSqlPersistence(
connectionString: connectionString,
providerName: ProviderName.SqlServer2022,
databaseMapping: DatabaseMapping.SqlServer,
tagStorageMode: TagMode.TagTable,
deleteCompatibilityMode: true,
useWriterUuidColumn: true,
autoInitialize: true,
journalBuilder: journalBuilder =>
{
journalBuilder.WithHealthCheck(name: "Akka.Persistence.Sql.Journal[default]");
},
snapshotBuilder: snapshotBuilder =>
{
snapshotBuilder.WithHealthCheck(name: "Akka.Persistence.Sql.SnapshotStore[default]");
});
// Add your actors here
// Example: builder.WithActors((system, registry) => { ... });
additionalConfig(builder, provider);
});
return services;
}
public static AkkaSettings BindAkkaSettings(IServiceCollection services, IConfiguration configuration)
{
var akkaSettings = new AkkaSettings();
configuration.GetSection(nameof(AkkaSettings)).Bind(akkaSettings);
services.AddSingleton(akkaSettings);
return akkaSettings;
}
public static AkkaConfigurationBuilder ConfigureNetwork(
this AkkaConfigurationBuilder builder,
IServiceProvider serviceProvider)
{
var settings = serviceProvider.GetRequiredService<AkkaSettings>();
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
// Apply TLS configuration if enabled
if (settings.TlsSettings is { Enabled: true })
{
ConfigureRemoteOptionsWithTls(settings);
}
builder.WithRemoting(settings.RemoteOptions);
if (settings.AkkaManagementOptions is { Enabled: true })
{
// Clear seed nodes when using Akka.Management
var clusterOptions = settings.ClusterOptions;
clusterOptions.SeedNodes = [];
builder
.WithClustering(clusterOptions)
.WithAkkaManagement(setup =>
{
setup.Http.HostName = settings.AkkaManagementOptions.Hostname?.ToLower();
setup.Http.Port = settings.AkkaManagementOptions.Port;
setup.Http.BindHostName = "0.0.0.0";
setup.Http.BindPort = settings.AkkaManagementOptions.Port;
})
.WithClusterBootstrap(options =>
{
options.ContactPointDiscovery.ServiceName = settings.AkkaManagementOptions.ServiceName;
options.ContactPointDiscovery.PortName = settings.AkkaManagementOptions.PortName;
options.ContactPointDiscovery.RequiredContactPointsNr =
settings.AkkaManagementOptions.RequiredContactPointsNr;
options.ContactPointDiscovery.ContactWithAllContactPoints = true;
options.ContactPointDiscovery.StableMargin = TimeSpan.FromSeconds(5);
options.ContactPoint.FilterOnFallbackPort =
settings.AkkaManagementOptions.FilterOnFallbackPort;
}, autoStart: true);
ConfigureDiscovery(builder, settings, configuration);
}
else
{
builder.WithClustering(settings.ClusterOptions);
}
return builder;
}
private static void ConfigureDiscovery(
AkkaConfigurationBuilder builder,
AkkaSettings settings,
IConfiguration configuration)
{
switch (settings.AkkaManagementOptions!.DiscoveryMethod)
{
case DiscoveryMethod.Kubernetes:
builder.WithKubernetesDiscovery();
break;
case DiscoveryMethod.AzureTableStorage:
var connectionString = configuration.GetConnectionString("AkkaManagementAzure");
if (connectionString is null)
throw new Exception("AkkaManagement table storage connection string [AkkaManagementAzure] is missing");
builder
.WithAzureDiscovery(options =>
{
options.ServiceName = settings.AkkaManagementOptions.ServiceName;
options.ConnectionString = connectionString;
options.HostName = settings.RemoteOptions.PublicHostName?.ToLower() ?? "localhost";
options.Port = settings.AkkaManagementOptions.Port;
})
.AddHocon(AzureDiscovery.DefaultConfiguration(), HoconAddMode.Append);
break;
case DiscoveryMethod.Config:
builder.WithConfigDiscovery(options =>
{
options.Services.Add(new Service
{
Name = settings.AkkaManagementOptions.ServiceName,
Endpoints =
[
$"{settings.AkkaManagementOptions.Hostname}:{settings.AkkaManagementOptions.Port}"
]
});
});
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private static void ConfigureRemoteOptionsWithTls(AkkaSettings settings)
{
var tlsSettings = settings.TlsSettings!;
var remoteOptions = settings.RemoteOptions;
var certificate = tlsSettings.LoadCertificate();
if (certificate is null)
throw new InvalidOperationException("TLS is enabled but no certificate could be loaded");
remoteOptions.EnableSsl = true;
remoteOptions.Ssl = new SslOptions
{
X509Certificate = certificate,
SuppressValidation = !tlsSettings.ValidateCertificates
};
// Update seed nodes to use akka.ssl.tcp:// protocol
if (settings.ClusterOptions.SeedNodes?.Length > 0)
{
settings.ClusterOptions.SeedNodes = settings.ClusterOptions.SeedNodes
.Select(node => node.Replace("akka.tcp://", "akka.ssl.tcp://"))
.ToArray();
}
}
}csharp
using Akka.Cluster.Hosting;
using Akka.Discovery.Azure;
using Akka.Discovery.Config.Hosting;
using Akka.Discovery.KubernetesApi;
using Akka.Hosting;
using Akka.Management;
using Akka.Management.Cluster.Bootstrap;
using Akka.Persistence.Sql.Config;
using Akka.Persistence.Sql.Hosting;
using Akka.Remote.Hosting;
using LinqToDB;
namespace YourApp.Config;
public static class AkkaConfiguration
{
public static IServiceCollection ConfigureAkka(
this IServiceCollection services,
IConfiguration configuration,
Action<AkkaConfigurationBuilder, IServiceProvider> additionalConfig)
{
var akkaSettings = BindAkkaSettings(services, configuration);
var connectionString = configuration.GetConnectionString("DefaultConnection");
if (connectionString is null)
throw new Exception("DefaultConnection ConnectionString is missing");
const string roleName = "your-role";
services.AddAkka(akkaSettings.ActorSystemName, (builder, provider) =>
{
builder.ConfigureNetwork(provider)
.WithAkkaClusterReadinessCheck()
.WithActorSystemLivenessCheck()
.WithSqlPersistence(
connectionString: connectionString,
providerName: ProviderName.SqlServer2022,
databaseMapping: DatabaseMapping.SqlServer,
tagStorageMode: TagMode.TagTable,
deleteCompatibilityMode: true,
useWriterUuidColumn: true,
autoInitialize: true,
journalBuilder: journalBuilder =>
{
journalBuilder.WithHealthCheck(name: "Akka.Persistence.Sql.Journal[default]");
},
snapshotBuilder: snapshotBuilder =>
{
snapshotBuilder.WithHealthCheck(name: "Akka.Persistence.Sql.SnapshotStore[default]");
});
// 在此处添加你的Actor
// 示例: builder.WithActors((system, registry) => { ... });
additionalConfig(builder, provider);
});
return services;
}
public static AkkaSettings BindAkkaSettings(IServiceCollection services, IConfiguration configuration)
{
var akkaSettings = new AkkaSettings();
configuration.GetSection(nameof(AkkaSettings)).Bind(akkaSettings);
services.AddSingleton(akkaSettings);
return akkaSettings;
}
public static AkkaConfigurationBuilder ConfigureNetwork(
this AkkaConfigurationBuilder builder,
IServiceProvider serviceProvider)
{
var settings = serviceProvider.GetRequiredService<AkkaSettings>();
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
// 如果启用则应用TLS配置
if (settings.TlsSettings is { Enabled: true })
{
ConfigureRemoteOptionsWithTls(settings);
}
builder.WithRemoting(settings.RemoteOptions);
if (settings.AkkaManagementOptions is { Enabled: true })
{
// 使用Akka.Management时清空种子节点
var clusterOptions = settings.ClusterOptions;
clusterOptions.SeedNodes = [];
builder
.WithClustering(clusterOptions)
.WithAkkaManagement(setup =>
{
setup.Http.HostName = settings.AkkaManagementOptions.Hostname?.ToLower();
setup.Http.Port = settings.AkkaManagementOptions.Port;
setup.Http.BindHostName = "0.0.0.0";
setup.Http.BindPort = settings.AkkaManagementOptions.Port;
})
.WithClusterBootstrap(options =>
{
options.ContactPointDiscovery.ServiceName = settings.AkkaManagementOptions.ServiceName;
options.ContactPointDiscovery.PortName = settings.AkkaManagementOptions.PortName;
options.ContactPointDiscovery.RequiredContactPointsNr =
settings.AkkaManagementOptions.RequiredContactPointsNr;
options.ContactPointDiscovery.ContactWithAllContactPoints = true;
options.ContactPointDiscovery.StableMargin = TimeSpan.FromSeconds(5);
options.ContactPoint.FilterOnFallbackPort =
settings.AkkaManagementOptions.FilterOnFallbackPort;
}, autoStart: true);
ConfigureDiscovery(builder, settings, configuration);
}
else
{
builder.WithClustering(settings.ClusterOptions);
}
return builder;
}
private static void ConfigureDiscovery(
AkkaConfigurationBuilder builder,
AkkaSettings settings,
IConfiguration configuration)
{
switch (settings.AkkaManagementOptions!.DiscoveryMethod)
{
case DiscoveryMethod.Kubernetes:
builder.WithKubernetesDiscovery();
break;
case DiscoveryMethod.AzureTableStorage:
var connectionString = configuration.GetConnectionString("AkkaManagementAzure");
if (connectionString is null)
throw new Exception("AkkaManagement表存储连接字符串[AkkaManagementAzure]缺失");
builder
.WithAzureDiscovery(options =>
{
options.ServiceName = settings.AkkaManagementOptions.ServiceName;
options.ConnectionString = connectionString;
options.HostName = settings.RemoteOptions.PublicHostName?.ToLower() ?? "localhost";
options.Port = settings.AkkaManagementOptions.Port;
})
.AddHocon(AzureDiscovery.DefaultConfiguration(), HoconAddMode.Append);
break;
case DiscoveryMethod.Config:
builder.WithConfigDiscovery(options =>
{
options.Services.Add(new Service
{
Name = settings.AkkaManagementOptions.ServiceName,
Endpoints =
[
$"{settings.AkkaManagementOptions.Hostname}:{settings.AkkaManagementOptions.Port}"
]
});
});
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private static void ConfigureRemoteOptionsWithTls(AkkaSettings settings)
{
var tlsSettings = settings.TlsSettings!;
var remoteOptions = settings.RemoteOptions;
var certificate = tlsSettings.LoadCertificate();
if (certificate is null)
throw new InvalidOperationException("已启用TLS但无法加载证书");
remoteOptions.EnableSsl = true;
remoteOptions.Ssl = new SslOptions
{
X509Certificate = certificate,
SuppressValidation = !tlsSettings.ValidateCertificates
};
// 更新种子节点以使用akka.ssl.tcp://协议
if (settings.ClusterOptions.SeedNodes?.Length > 0)
{
settings.ClusterOptions.SeedNodes = settings.ClusterOptions.SeedNodes
.Select(node => node.Replace("akka.tcp://", "akka.ssl.tcp://"))
.ToArray();
}
}
}Program.cs Integration
Program.cs集成
csharp
using YourApp.Config;
using Petabridge.Cmd.Host;
using Petabridge.Cmd.Cluster;
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddRazorPages(); // or whatever your app needs
// Configure Akka.NET
builder.Services.ConfigureAkka(builder.Configuration,
(configurationBuilder, provider) =>
{
var options = provider.GetRequiredService<AkkaSettings>();
// Add Petabridge.Cmd for cluster management
configurationBuilder.AddPetabridgeCmd(
options: options.PbmOptions,
hostConfiguration: cmd =>
{
cmd.RegisterCommandPalette(ClusterCommands.Instance);
});
});
var app = builder.Build();
// Configure middleware
app.MapRazorPages();
app.Run();csharp
using YourApp.Config;
using Petabridge.Cmd.Host;
using Petabridge.Cmd.Cluster;
var builder = WebApplication.CreateBuilder(args);
// 添加服务
builder.Services.AddRazorPages(); // 或你的应用所需的其他服务
// 配置Akka.NET
builder.Services.ConfigureAkka(builder.Configuration,
(configurationBuilder, provider) =>
{
var options = provider.GetRequiredService<AkkaSettings>();
// 添加Petabridge.Cmd用于集群管理
configurationBuilder.AddPetabridgeCmd(
options: options.PbmOptions,
hostConfiguration: cmd =>
{
cmd.RegisterCommandPalette(ClusterCommands.Instance);
});
});
var app = builder.Build();
// 配置中间件
app.MapRazorPages();
app.Run();Aspire AppHost Configuration (Program.cs)
Aspire AppHost配置(Program.cs)
csharp
using System.Net.Sockets;
var builder = DistributedApplication.CreateBuilder(args);
var config = builder.Configuration.GetSection("YourApp")
.Get<YourAppConfiguration>() ?? new YourAppConfiguration();
var saPassword = builder.AddParameter(
"sql-sa-password",
() => "YourStrong!Passw0rd",
secret: true);
var sqlServer = builder.AddSqlServer("sql", saPassword);
if (config.UseVolumes)
{
sqlServer.WithDataVolume();
}
var db = sqlServer.AddDatabase("YourDb");
var app = builder.AddProject<Projects.YourApp>("yourapp")
.WithReplicas(config.Replicas)
.WithReference(db, "DefaultConnection")
.ConfigureAkkaManagementForApp(config);
builder.Build().Run();
public class YourAppConfiguration
{
public int Replicas { get; set; } = 1;
public bool UseVolumes { get; set; } = false;
public bool UseAkkaManagement { get; set; } = false;
}csharp
using System.Net.Sockets;
var builder = DistributedApplication.CreateBuilder(args);
var config = builder.Configuration.GetSection("YourApp")
.Get<YourAppConfiguration>() ?? new YourAppConfiguration();
var saPassword = builder.AddParameter(
"sql-sa-password",
() => "YourStrong!Passw0rd",
secret: true);
var sqlServer = builder.AddSqlServer("sql", saPassword);
if (config.UseVolumes)
{
sqlServer.WithDataVolume();
}
var db = sqlServer.AddDatabase("YourDb");
var app = builder.AddProject<Projects.YourApp>("yourapp")
.WithReplicas(config.Replicas)
.WithReference(db, "DefaultConnection")
.ConfigureAkkaManagementForApp(config);
builder.Build().Run();
public class YourAppConfiguration
{
public int Replicas { get; set; } = 1;
public bool UseVolumes { get; set; } = false;
public bool UseAkkaManagement { get; set; } = false;
}Aspire Akka.Management Extensions (AkkaManagementExtensions.cs)
Aspire Akka.Management扩展(AkkaManagementExtensions.cs)
csharp
using System.Net.Sockets;
using Aspire.Hosting.Azure;
namespace YourApp.AppHost;
public static class AkkaManagementExtensions
{
public static IResourceBuilder<ProjectResource> ConfigureAkkaManagementForApp(
this IResourceBuilder<ProjectResource> appBuilder,
YourAppConfiguration config)
{
if (!config.UseAkkaManagement) return appBuilder;
var builder = appBuilder.ApplicationBuilder;
// Setup Azure Table Storage for discovery
var azureStorage = builder.AddAzureStorage("storage")
.RunAsEmulator();
var tableStorage = azureStorage.AddTables("akka-discovery");
appBuilder.WaitFor(tableStorage)
.WithReference(tableStorage, "AkkaManagementAzure");
// Setup network endpoint ports
appBuilder
.WithEndpoint(name: "remote", protocol: ProtocolType.Tcp,
env: "AkkaSettings__RemoteOptions__Port")
.WithEndpoint(name: "management", protocol: ProtocolType.Tcp,
env: "AkkaSettings__AkkaManagementOptions__Port")
.WithEndpoint(name: "pbm", protocol: ProtocolType.Tcp,
env: "AkkaSettings__PbmOptions__Port");
// Configure Akka.Management settings via environment variables
appBuilder
.WithEnvironment("AkkaSettings__RemoteOptions__PublicHostName", "localhost")
.WithEnvironment("AkkaSettings__AkkaManagementOptions__Enabled", "true")
.WithEnvironment("AkkaSettings__AkkaManagementOptions__Hostname", "localhost")
.WithEnvironment("AkkaSettings__AkkaManagementOptions__DiscoveryMethod", "AzureTableStorage")
.WithEnvironment("AkkaSettings__AkkaManagementOptions__RequiredContactPointsNr",
config.Replicas.ToString())
.WithEnvironment("AkkaSettings__AkkaManagementOptions__FilterOnFallbackPort", "false");
return appBuilder;
}
}csharp
using System.Net.Sockets;
using Aspire.Hosting.Azure;
namespace YourApp.AppHost;
public static class AkkaManagementExtensions
{
public static IResourceBuilder<ProjectResource> ConfigureAkkaManagementForApp(
this IResourceBuilder<ProjectResource> appBuilder,
YourAppConfiguration config)
{
if (!config.UseAkkaManagement) return appBuilder;
var builder = appBuilder.ApplicationBuilder;
// 搭建Azure Table Storage用于服务发现
var azureStorage = builder.AddAzureStorage("storage")
.RunAsEmulator();
var tableStorage = azureStorage.AddTables("akka-discovery");
appBuilder.WaitFor(tableStorage)
.WithReference(tableStorage, "AkkaManagementAzure");
// 配置网络端点端口
appBuilder
.WithEndpoint(name: "remote", protocol: ProtocolType.Tcp,
env: "AkkaSettings__RemoteOptions__Port")
.WithEndpoint(name: "management", protocol: ProtocolType.Tcp,
env: "AkkaSettings__AkkaManagementOptions__Port")
.WithEndpoint(name: "pbm", protocol: ProtocolType.Tcp,
env: "AkkaSettings__PbmOptions__Port");
// 通过环境变量配置Akka.Management设置
appBuilder
.WithEnvironment("AkkaSettings__RemoteOptions__PublicHostName", "localhost")
.WithEnvironment("AkkaSettings__AkkaManagementOptions__Enabled", "true")
.WithEnvironment("AkkaSettings__AkkaManagementOptions__Hostname", "localhost")
.WithEnvironment("AkkaSettings__AkkaManagementOptions__DiscoveryMethod", "AzureTableStorage")
.WithEnvironment("AkkaSettings__AkkaManagementOptions__RequiredContactPointsNr",
config.Replicas.ToString())
.WithEnvironment("AkkaSettings__AkkaManagementOptions__FilterOnFallbackPort", "false");
return appBuilder;
}
}appsettings.json Configuration
appsettings.json配置
json
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=YourDb;User Id=sa;Password=YourStrong!Passw0rd;"
},
"AkkaSettings": {
"ActorSystemName": "YourSystem",
"LogConfigOnStart": false,
"RemoteOptions": {
"PublicHostName": null,
"HostName": "0.0.0.0",
"Port": 8081
},
"ClusterOptions": {
"Roles": ["your-role"],
"SeedNodes": []
},
"PbmOptions": {
"Host": "0.0.0.0",
"Port": 9110
}
}
}json
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=YourDb;User Id=sa;Password=YourStrong!Passw0rd;"
},
"AkkaSettings": {
"ActorSystemName": "YourSystem",
"LogConfigOnStart": false,
"RemoteOptions": {
"PublicHostName": null,
"HostName": "0.0.0.0",
"Port": 8081
},
"ClusterOptions": {
"Roles": ["your-role"],
"SeedNodes": []
},
"PbmOptions": {
"Host": "0.0.0.0",
"Port": 9110
}
}
}Common Patterns
常见模式
Pattern 1: Actor Registration with Dependency Injection
模式1:依赖注入式Actor注册
csharp
// In your actor project
public static class ActorRegistration
{
public static AkkaConfigurationBuilder AddYourActor(
this AkkaConfigurationBuilder builder,
string roleName)
{
builder.WithActors((system, registry, resolver) =>
{
var props = resolver.Props<YourActor>();
var actor = system.ActorOf(props, "your-actor");
registry.Register<YourActor>(actor);
});
return builder;
}
}
// In AkkaConfiguration.cs
builder
.ConfigureNetwork(provider)
.WithSqlPersistence(...)
.AddYourActor(roleName); // Register your actorcsharp
// 在你的Actor项目中
public static class ActorRegistration
{
public static AkkaConfigurationBuilder AddYourActor(
this AkkaConfigurationBuilder builder,
string roleName)
{
builder.WithActors((system, registry, resolver) =>
{
var props = resolver.Props<YourActor>();
var actor = system.ActorOf(props, "your-actor");
registry.Register<YourActor>(actor);
});
return builder;
}
}
// 在AkkaConfiguration.cs中
builder
.ConfigureNetwork(provider)
.WithSqlPersistence(...)
.AddYourActor(roleName); // 注册你的ActorPattern 2: Cluster Sharding Setup
模式2:集群分片设置
csharp
builder.WithShardRegion<YourEntityActor>(
typeName: "your-entity",
entityPropsFactory: (_, _, resolver) => resolver.Props<YourEntityActor>(),
extractEntityId: ExtractEntityId,
extractShardId: ExtractShardId,
shardOptions: new ShardOptions
{
Role = "your-role",
StateStoreMode = StateStoreMode.Persistence
});
private static string ExtractEntityId(object message)
{
return message switch
{
IEntityMessage msg => msg.EntityId,
_ => null
};
}
private static string ExtractShardId(object message)
{
return message switch
{
IEntityMessage msg => (msg.EntityId.GetHashCode() % 10).ToString(),
_ => null
};
}csharp
builder.WithShardRegion<YourEntityActor>(
typeName: "your-entity",
entityPropsFactory: (_, _, resolver) => resolver.Props<YourEntityActor>(),
extractEntityId: ExtractEntityId,
extractShardId: ExtractShardId,
shardOptions: new ShardOptions
{
Role = "your-role",
StateStoreMode = StateStoreMode.Persistence
});
private static string ExtractEntityId(object message)
{
return message switch
{
IEntityMessage msg => msg.EntityId,
_ => null
};
}
private static string ExtractShardId(object message)
{
return message switch
{
IEntityMessage msg => (msg.EntityId.GetHashCode() % 10).ToString(),
_ => null
};
}Pattern 3: Health Checks
模式3:健康检查
Always configure health checks in Program.cs:
csharp
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy("Application is running"),
tags: new[] { "liveness" });
// Akka health checks are added automatically by:
// - .WithAkkaClusterReadinessCheck()
// - .WithActorSystemLivenessCheck()
// - journalBuilder.WithHealthCheck()
// - snapshotBuilder.WithHealthCheck()始终在Program.cs中配置健康检查:
csharp
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy("Application is running"),
tags: new[] { "liveness" });
// Akka健康检查会通过以下方式自动添加:
// - .WithAkkaClusterReadinessCheck()
// - .WithActorSystemLivenessCheck()
// - journalBuilder.WithHealthCheck()
// - snapshotBuilder.WithHealthCheck()Common Issues and Solutions
常见问题与解决方案
Issue 1: Cluster Nodes Can't Discover Each Other
问题1:集群节点无法相互发现
Symptoms: Nodes stay as "Unreachable" in cluster status
Solution:
- Verify matches the number of replicas
RequiredContactPointsNr - Check that all nodes use the same in AkkaManagementOptions
ServiceName - Ensure Azure Table Storage connection string is correct
- Verify firewall/network allows TCP on remote and management ports
症状: 节点在集群状态中显示为"Unreachable"
解决方案:
- 验证与副本数量匹配
RequiredContactPointsNr - 检查所有节点在AkkaManagementOptions中使用相同的
ServiceName - 确保Azure Table Storage连接字符串正确
- 验证防火墙/网络允许远程端口与管理端口的TCP通信
Issue 2: Persistence Initialization Fails
问题2:持久化初始化失败
Symptoms: Application fails to start with SQL connection errors
Solution:
- Ensure SQL Server is running (check Aspire dashboard)
- Verify connection string is correctly configured
- Set in WithSqlPersistence
autoInitialize: true - Check that database exists and is accessible
症状: 应用启动失败并出现SQL连接错误
解决方案:
- 确保SQL Server正在运行(检查Aspire仪表板)
- 验证连接字符串配置正确
- 在WithSqlPersistence中设置
autoInitialize: true - 检查数据库是否存在且可访问
Issue 3: Split Brain in Development
问题3:开发环境中出现脑裂
Symptoms: Multiple separate clusters form instead of one unified cluster
Solution:
- Use in local development
FilterOnFallbackPort = false - Ensure all replicas use the same discovery configuration
- Set
ContactWithAllContactPoints = true - Increase for slower dev machines
StableMargin
症状: 形成多个独立集群而非统一集群
解决方案:
- 在本地开发中设置
FilterOnFallbackPort = false - 确保所有副本使用相同的服务发现配置
- 设置
ContactWithAllContactPoints = true - 为性能较慢的开发机器增加
StableMargin
Testing Akka.NET Actors
测试Akka.NET Actors
For comprehensive Akka.NET testing patterns using Akka.Hosting.TestKit, see the skill.
akka-net-testing-patternsThat skill covers:
- Modern testing with Akka.Hosting.TestKit and dependency injection
- TestProbe patterns for verifying actor interactions
- Testing persistent actors and event sourcing
- Local cluster sharding tests with
AkkaExecutionMode.LocalTest - Scenario-based integration tests
- Best practices and anti-patterns
如需了解使用Akka.Hosting.TestKit的Akka.NET综合测试模式,请查看技能。
akka-net-testing-patterns该技能涵盖:
- 使用Akka.Hosting.TestKit与依赖注入的现代测试方法
- 用于验证Actor交互的TestProbe模式
- 测试持久化Actor与事件溯源
- 使用的本地集群分片测试
AkkaExecutionMode.LocalTest - 基于场景的集成测试
- 最佳实践与反模式
Quick Example: Testing Akka + Aspire Integration
快速示例:测试Akka + Aspire集成
When testing Aspire applications with Akka.NET actors, combine patterns with :
aspire-integration-testingakka-net-testing-patternscsharp
// Use Aspire's DistributedApplicationTestingBuilder for infrastructure
// Use Akka.Hosting.TestKit for actor testing
public class AkkaAspireIntegrationTests : IAsyncLifetime
{
private DistributedApplication? _app;
public async Task InitializeAsync()
{
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.YourApp_AppHost>();
_app = await appHost.BuildAsync();
await _app.StartAsync();
}
[Fact]
public async Task ActorSystem_WithRealDatabase_ShouldPersistEvents()
{
// Get SQL connection string from Aspire
var dbResource = _app!.GetResource("yourdb");
var connectionString = await dbResource.GetConnectionStringAsync();
// Create HttpClient to test actor endpoints
var httpClient = _app.CreateHttpClient("yourapp");
// Test actor behavior through HTTP API
var response = await httpClient.PostAsJsonAsync("/orders", new
{
OrderId = "ORDER-001",
Amount = 100.00m
});
response.Should().BeSuccessStatusCode();
// Verify data was persisted to real database
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
var events = await connection.QueryAsync<string>(
"SELECT EventType FROM EventJournal WHERE PersistenceId = 'order-ORDER-001'");
events.Should().Contain("OrderCreated");
}
public async Task DisposeAsync()
{
if (_app is not null)
await _app.DisposeAsync();
}
}For unit testing individual actors, use with in-memory persistence (no Aspire needed).
akka-net-testing-patterns测试带Akka.NET Actors的Aspire应用时,将模式与结合使用:
aspire-integration-testingakka-net-testing-patternscsharp
// 使用Aspire的DistributedApplicationTestingBuilder搭建基础设施
// 使用Akka.Hosting.TestKit进行Actor测试
public class AkkaAspireIntegrationTests : IAsyncLifetime
{
private DistributedApplication? _app;
public async Task InitializeAsync()
{
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.YourApp_AppHost>();
_app = await appHost.BuildAsync();
await _app.StartAsync();
}
[Fact]
public async Task ActorSystem_WithRealDatabase_ShouldPersistEvents()
{
// 从Aspire获取SQL连接字符串
var dbResource = _app!.GetResource("yourdb");
var connectionString = await dbResource.GetConnectionStringAsync();
// 创建HttpClient以测试Actor端点
var httpClient = _app.CreateHttpClient("yourapp");
// 通过HTTP API测试Actor行为
var response = await httpClient.PostAsJsonAsync("/orders", new
{
OrderId = "ORDER-001",
Amount = 100.00m
});
response.Should().BeSuccessStatusCode();
// 验证数据是否已持久化至真实数据库
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
var events = await connection.QueryAsync<string>(
"SELECT EventType FROM EventJournal WHERE PersistenceId = 'order-ORDER-001'");
events.Should().Contain("OrderCreated");
}
public async Task DisposeAsync()
{
if (_app is not null)
await _app.DisposeAsync();
}
}如需单元测试单个Actor,使用结合内存持久化(无需Aspire)。
akka-net-testing-patternsBest Practices Summary
最佳实践总结
- Always use health checks - Configure readiness and liveness checks for all components
- Bind settings from configuration - Never hard-code hostnames, ports, or connection strings
- Use Akka.Management for multi-node - Don't use static seed nodes for clusters with >1 replica
- Configure TLS for production - Always use TLS in production environments
- Separate actor logic from configuration - Keep actors pure and configuration in extension methods
- Use Petabridge.Cmd - Essential for debugging and managing clusters
- Test with multiple replicas - Always test with to catch clustering issues
Replicas > 1 - Monitor persistence health - Configure health checks for journal and snapshot stores
- 始终使用健康检查 - 为所有组件配置就绪性与存活性检查
- 从配置绑定设置 - 切勿硬编码主机名、端口或连接字符串
- 多节点场景使用Akka.Management - 对于副本数>1的集群,不要使用静态种子节点
- 生产环境配置TLS - 生产环境中始终启用TLS
- 分离Actor逻辑与配置 - 保持Actor纯逻辑,配置置于扩展方法中
- 使用Petabridge.Cmd - 这是调试与管理集群的必备工具
- 使用多副本测试 - 始终使用测试以发现集群问题
Replicas > 1 - 监控持久化健康状态 - 为日志与快照存储配置健康检查