akka-hosting-actor-patterns by aaronontheweb/dotnet-skills
npx skills add https://github.com/aaronontheweb/dotnet-skills --skill akka-hosting-actor-patterns在以下场景中使用此技能:
定义一个枚举来控制 Actor 行为:
/// <summary>
/// 决定如何配置 Akka.NET
/// </summary>
public enum AkkaExecutionMode
{
/// <summary>
/// 纯本地 Actor 系统 - 无远程通信,无集群。
/// 使用 GenericChildPerEntityParent 代替 ShardRegion。
/// 适用于单元测试和简单场景。
/// </summary>
LocalTest,
/// <summary>
/// 使用 ShardRegion 的完整集群模式。
/// 用于集成测试和生产环境。
/// </summary>
Clustered
}
一个轻量级的父级 Actor,将消息路由到子实体,模拟集群分片语义而无需集群:
using Akka.Actor;
using Akka.Cluster.Sharding;
/// <summary>
/// 一个通用的“每个实体一个子级”的父级 Actor。
/// </summary>
/// <remarks>
/// 复用 Akka.Cluster.Sharding 的 IMessageExtractor 以实现一致的路由。
/// 适用于不需要集群开销的单元测试。
/// </remarks>
public sealed class GenericChildPerEntityParent : ReceiveActor
{
public static Props CreateProps(
IMessageExtractor extractor,
Func<string, Props> propsFactory)
{
return Props.Create(() =>
new GenericChildPerEntityParent(extractor, propsFactory));
}
private readonly IMessageExtractor _extractor;
private readonly Func<string, Props> _propsFactory;
public GenericChildPerEntityParent(
IMessageExtractor extractor,
Func<string, Props> propsFactory)
{
_extractor = extractor;
_propsFactory = propsFactory;
ReceiveAny(message =>
{
var entityId = _extractor.EntityId(message);
if (entityId is null) return;
// 获取现有子级或创建新子级
Context.Child(entityId)
.GetOrElse(() => Context.ActorOf(_propsFactory(entityId), entityId))
.Forward(_extractor.EntityMessage(message));
});
}
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
创建实现 Akka.Cluster.Sharding 中 IMessageExtractor 的提取器:
using Akka.Cluster.Sharding;
/// <summary>
/// 基于强类型 ID 将消息路由到实体 Actor。
/// </summary>
public sealed class OrderMessageExtractor : HashCodeMessageExtractor
{
public const int DefaultShardCount = 40;
public OrderMessageExtractor(int maxNumberOfShards = DefaultShardCount)
: base(maxNumberOfShards)
{
}
public override string? EntityId(object message)
{
return message switch
{
IWithOrderId msg => msg.OrderId.Value.ToString(),
_ => null
};
}
}
// 为针对特定实体的消息定义一个接口
public interface IWithOrderId
{
OrderId OrderId { get; }
}
// 使用强类型 ID
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString();
}
创建抽象执行模式的扩展方法:
using Akka.Cluster.Hosting;
using Akka.Cluster.Sharding;
using Akka.Hosting;
public static class OrderActorHostingExtensions
{
/// <summary>
/// 添加 OrderActor,支持本地和集群两种模式。
/// </summary>
public static AkkaConfigurationBuilder WithOrderActor(
this AkkaConfigurationBuilder builder,
AkkaExecutionMode executionMode = AkkaExecutionMode.Clustered,
string? clusterRole = null)
{
if (executionMode == AkkaExecutionMode.LocalTest)
{
// 非集群模式:使用 GenericChildPerEntityParent
builder.WithActors((system, registry, resolver) =>
{
var parent = system.ActorOf(
GenericChildPerEntityParent.CreateProps(
new OrderMessageExtractor(),
entityId => resolver.Props<OrderActor>(entityId)),
"orders");
registry.Register<OrderActor>(parent);
});
}
else
{
// 集群模式:使用 ShardRegion
builder.WithShardRegion<OrderActor>(
"orders",
(system, registry, resolver) =>
entityId => resolver.Props<OrderActor>(entityId),
new OrderMessageExtractor(),
new ShardOptions
{
StateStoreMode = StateStoreMode.DData,
Role = clusterRole
});
}
return builder;
}
}
创建一个便捷方法来注册所有领域 Actor:
public static class DomainActorHostingExtensions
{
/// <summary>
/// 添加所有订单领域 Actor 并支持分片。
/// </summary>
public static AkkaConfigurationBuilder WithOrderDomainActors(
this AkkaConfigurationBuilder builder,
AkkaExecutionMode executionMode = AkkaExecutionMode.Clustered,
string? clusterRole = null)
{
return builder
.WithOrderActor(executionMode, clusterRole)
.WithPaymentActor(executionMode, clusterRole)
.WithShipmentActor(executionMode, clusterRole)
.WithNotificationActor(); // 单例,无需分片
}
}
将 ActorSystem 的 Scheduler 注册为 ITimeProvider,以实现可测试的基于时间的逻辑:
public static class SharedAkkaHostingExtensions
{
public static IServiceCollection AddAkkaWithTimeProvider(
this IServiceCollection services,
Action<AkkaConfigurationBuilder, IServiceProvider> configure)
{
// 使用 ActorSystem 的 Scheduler 注册 ITimeProvider
services.AddSingleton<ITimeProvider>(sp =>
sp.GetRequiredService<ActorSystem>().Scheduler);
return services.ConfigureAkka((builder, sp) =>
{
configure(builder, sp);
});
}
}
// 在你的 Actor 中,注入 ITimeProvider
public class SubscriptionActor : ReceiveActor
{
private readonly ITimeProvider _timeProvider;
public SubscriptionActor(ITimeProvider timeProvider)
{
_timeProvider = timeProvider;
// 使用 _timeProvider.GetUtcNow() 而不是 DateTime.UtcNow
// 这允许测试控制时间
}
}
对于重启后仍需保留的持久化定时任务,使用 akka-reminders:
using Akka.Reminders;
using Akka.Reminders.Sql;
using Akka.Reminders.Sql.Configuration;
using Akka.Reminders.Storage;
public static class ReminderHostingExtensions
{
/// <summary>
/// 使用 PostgreSQL 存储配置 akka-reminders。
/// </summary>
public static AkkaConfigurationBuilder WithPostgresReminders(
this AkkaConfigurationBuilder builder,
string connectionString,
string schemaName = "reminders",
string tableName = "scheduled_reminders",
bool autoInitialize = true)
{
return builder.WithLocalReminders(reminders => reminders
.WithResolver(sys => new GenericChildPerEntityResolver(sys))
.WithStorage(system =>
{
var settings = SqlReminderStorageSettings.CreatePostgreSql(
connectionString,
schemaName,
tableName,
autoInitialize);
return new SqlReminderStorage(settings, system);
})
.WithSettings(new ReminderSettings
{
MaxSlippage = TimeSpan.FromSeconds(30),
MaxDeliveryAttempts = 3,
RetryBackoffBase = TimeSpan.FromSeconds(10)
}));
}
/// <summary>
/// 为测试配置使用内存存储的 akka-reminders。
/// </summary>
public static AkkaConfigurationBuilder WithInMemoryReminders(
this AkkaConfigurationBuilder builder)
{
return builder.WithLocalReminders(reminders => reminders
.WithResolver(sys => new GenericChildPerEntityResolver(sys))
.WithStorage(system => new InMemoryReminderStorage())
.WithSettings(new ReminderSettings
{
MaxSlippage = TimeSpan.FromSeconds(1),
MaxDeliveryAttempts = 3,
RetryBackoffBase = TimeSpan.FromMilliseconds(100)
}));
}
}
将提醒回调路由到 GenericChildPerEntityParent Actor:
using Akka.Actor;
using Akka.Hosting;
using Akka.Reminders;
/// <summary>
/// 将提醒目标解析为 GenericChildPerEntityParent Actor。
/// </summary>
public sealed class GenericChildPerEntityResolver : IReminderActorResolver
{
private readonly ActorSystem _system;
public GenericChildPerEntityResolver(ActorSystem system)
{
_system = system;
}
public IActorRef ResolveActorRef(ReminderEntry entry)
{
var registry = ActorRegistry.For(_system);
return entry.Key switch
{
var k when k.StartsWith("order-") =>
registry.Get<OrderActor>(),
var k when k.StartsWith("subscription-") =>
registry.Get<SubscriptionActor>(),
_ => throw new InvalidOperationException(
$"未知的提醒键格式: {entry.Key}")
};
}
}
对于只应有一个实例的 Actor:
public static AkkaConfigurationBuilder WithEmailSenderActor(
this AkkaConfigurationBuilder builder)
{
return builder.WithActors((system, registry, resolver) =>
{
var actor = system.ActorOf(
resolver.Props<EmailSenderActor>(),
"email-sender");
registry.Register<EmailSenderActor>(actor);
});
}
当你需要引用注册为父级的 Actor 时:
/// <summary>
/// ActorRegistry 用于检索订单管理器的标记类型
/// (OrderActors 的 GenericChildPerEntityParent)。
/// </summary>
public sealed class OrderManagerActor;
// 在扩展方法中的用法
registry.Register<OrderManagerActor>(parent);
// 在控制器/服务中的用法
public class OrderService
{
private readonly IActorRef _orderManager;
public OrderService(IRequiredActor<OrderManagerActor> orderManager)
{
_orderManager = orderManager.ActorRef;
}
public async Task<OrderResponse> CreateOrder(CreateOrderCommand cmd)
{
return await _orderManager.Ask<OrderResponse>(cmd);
}
}
Actor 没有自动的 DI 作用域。 与 ASP.NET 控制器(每个 HTTP 请求创建一个作用域)不同,Actor 是长生命周期的。如果你需要作用域服务(如 DbContext),请注入 IServiceProvider 并手动创建作用域。
public sealed class OrderProcessingActor : ReceiveActor
{
private readonly IServiceProvider _serviceProvider;
private readonly IActorRef _notificationActor;
public OrderProcessingActor(
IServiceProvider serviceProvider,
IRequiredActor<NotificationActor> notificationActor)
{
_serviceProvider = serviceProvider;
_notificationActor = notificationActor.ActorRef;
ReceiveAsync<ProcessOrder>(HandleProcessOrder);
}
private async Task HandleProcessOrder(ProcessOrder msg)
{
// 为此消息创建作用域 - 处理完成后释放
using var scope = _serviceProvider.CreateScope();
// 在作用域内解析作用域服务
var orderRepository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
var paymentService = scope.ServiceProvider.GetRequiredService<IPaymentService>();
var emailComposer = scope.ServiceProvider.GetRequiredService<IOrderEmailComposer>();
// 使用作用域服务执行工作
var order = await orderRepository.GetByIdAsync(msg.OrderId);
var payment = await paymentService.ProcessAsync(order);
// 作用域释放时提交 DbContext 更改
}
}
| 优势 | 解释 |
|---|---|
| 每条消息使用新的 DbContext | 消息之间没有过时的实体跟踪 |
| 正确释放 | 每条消息后释放数据库连接 |
| 隔离性 | 一条消息的错误不会破坏另一条消息的状态 |
| 可测试 | 可以在测试中注入模拟的 IServiceProvider |
对于无状态、线程安全的服务,直接注入(无需作用域):
public sealed class NotificationActor : ReceiveActor
{
private readonly IEmailLinkGenerator _linkGenerator; // 单例 - 没问题!
private readonly IMjmlTemplateRenderer _renderer; // 单例 - 没问题!
public NotificationActor(
IEmailLinkGenerator linkGenerator,
IMjmlTemplateRenderer renderer)
{
_linkGenerator = linkGenerator;
_renderer = renderer;
Receive<SendWelcomeEmail>(Handle);
}
}
// 错误:将作用域服务注入长生命周期的 Actor
public sealed class BadActor : ReceiveActor
{
private readonly IOrderRepository _repo; // 作用域!DbContext 永远存活!
public BadActor(IOrderRepository repo) // 在 Actor 创建时捕获
{
_repo = repo; // 这个 DbContext 会变得过时
}
}
// 正确:注入 IServiceProvider,为每条消息创建作用域
public sealed class GoodActor : ReceiveActor
{
private readonly IServiceProvider _sp;
public GoodActor(IServiceProvider sp)
{
_sp = sp;
ReceiveAsync<ProcessOrder>(async msg =>
{
using var scope = _sp.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
// 为此消息使用新的 DbContext
});
}
}
有关 DI 生命周期和作用域管理的更多信息,请参阅 microsoft-extensions/dependency-injection 技能。
RememberEntities 控制分片区域是否记住并自动重启所有曾经创建的实体。这几乎应该总是设为 false。
builder.WithShardRegion<OrderActor>(
"orders",
(system, registry, resolver) => entityId => resolver.Props<OrderActor>(entityId),
new OrderMessageExtractor(),
new ShardOptions
{
StateStoreMode = StateStoreMode.DData,
RememberEntities = false, // 默认值 - 几乎总是正确的
Role = clusterRole
});
当 RememberEntities = true 导致的问题:
| 问题 | 解释 |
|---|---|
| 无限制的内存增长 | 每个曾经创建的实体都会被记住并永远重启 |
| 集群启动缓慢 | 集群必须在启动时重启成千上万/数百万个实体 |
| 过时实体复活 | 过期的会话、已发送的邮件、旧订单都会被重启 |
| 无钝化 | 空闲实体无限期占用内存(钝化被禁用) |
| 实体类型 | RememberEntities | 原因 |
|---|---|---|
UserSessionActor | false | 会话会过期,在登录时创建 |
DraftActor | false | 草稿会被发送/丢弃,是临时的 |
EmailSenderActor | false | 即发即弃的操作 |
OrderActor | false | 订单完成,新订单不断创建 |
ShoppingCartActor | false | 购物车会过期,废弃购物车很常见 |
TenantActor | 可能为 true | 固定的租户集合,总是需要 |
AccountActor | 可能为 true | 有界的账户集合,长生命周期 |
经验法则: 仅在以下情况下使用 RememberEntities = true:
当使用 WithShardRegion<T> 时,泛型参数 T 作为 ActorRegistry 的标记类型。使用专用的标记类型(而非 Actor 类本身)以实现一致的注册表访问:
/// <summary>
/// ActorRegistry 的标记类型。使用此类型来检索 OrderActor 分片区域。
/// </summary>
public sealed class OrderActorRegion;
// 注册 - 使用标记类型作为泛型参数
builder.WithShardRegion<OrderActorRegion>(
"orders",
(system, registry, resolver) => entityId => resolver.Props<OrderActor>(entityId),
new OrderMessageExtractor(),
new ShardOptions { StateStoreMode = StateStoreMode.DData });
// 检索 - 使用相同的标记类型
var orderRegion = ActorRegistry.Get<OrderActorRegion>();
orderRegion.Tell(new CreateOrder(orderId, amount));
为何使用标记类型?
WithShardRegion<T> 自动将分片区域注册到类型 T 下WithShardRegion<T> 会自动将分片区域注册到 ActorRegistry 中。不要再调用 registry.Register<T>():
// 错误 - 冗余注册
builder.WithShardRegion<OrderActorRegion>("orders", ...)
.WithActors((system, registry, resolver) =>
{
var region = registry.Get<OrderActorRegion>();
registry.Register<OrderActorRegion>(region); // 不必要!
});
// 正确 - WithShardRegion 已经注册
builder.WithShardRegion<OrderActorRegion>("orders", ...);
// 就这样 - OrderActorRegion 现在已在注册表中
OrderId 而非 string 或 GuidIWithOrderId 进行类型安全的提取DateTime.Now每周安装量
74
代码仓库
GitHub 星标数
488
首次出现
2026年1月28日
安全审计
安装于
claude-code57
codex47
opencode46
gemini-cli44
github-copilot44
kimi-cli41
Use this skill when:
Define an enum to control actor behavior:
/// <summary>
/// Determines how Akka.NET should be configured
/// </summary>
public enum AkkaExecutionMode
{
/// <summary>
/// Pure local actor system - no remoting, no clustering.
/// Use GenericChildPerEntityParent instead of ShardRegion.
/// Ideal for unit tests and simple scenarios.
/// </summary>
LocalTest,
/// <summary>
/// Full clustering with ShardRegion.
/// Use for integration testing and production.
/// </summary>
Clustered
}
A lightweight parent actor that routes messages to child entities, mimicking cluster sharding semantics without requiring a cluster:
using Akka.Actor;
using Akka.Cluster.Sharding;
/// <summary>
/// A generic "child per entity" parent actor.
/// </summary>
/// <remarks>
/// Reuses Akka.Cluster.Sharding's IMessageExtractor for consistent routing.
/// Ideal for unit tests where clustering overhead is unnecessary.
/// </remarks>
public sealed class GenericChildPerEntityParent : ReceiveActor
{
public static Props CreateProps(
IMessageExtractor extractor,
Func<string, Props> propsFactory)
{
return Props.Create(() =>
new GenericChildPerEntityParent(extractor, propsFactory));
}
private readonly IMessageExtractor _extractor;
private readonly Func<string, Props> _propsFactory;
public GenericChildPerEntityParent(
IMessageExtractor extractor,
Func<string, Props> propsFactory)
{
_extractor = extractor;
_propsFactory = propsFactory;
ReceiveAny(message =>
{
var entityId = _extractor.EntityId(message);
if (entityId is null) return;
// Get existing child or create new one
Context.Child(entityId)
.GetOrElse(() => Context.ActorOf(_propsFactory(entityId), entityId))
.Forward(_extractor.EntityMessage(message));
});
}
}
Create extractors that implement IMessageExtractor from Akka.Cluster.Sharding:
using Akka.Cluster.Sharding;
/// <summary>
/// Routes messages to entity actors based on a strongly-typed ID.
/// </summary>
public sealed class OrderMessageExtractor : HashCodeMessageExtractor
{
public const int DefaultShardCount = 40;
public OrderMessageExtractor(int maxNumberOfShards = DefaultShardCount)
: base(maxNumberOfShards)
{
}
public override string? EntityId(object message)
{
return message switch
{
IWithOrderId msg => msg.OrderId.Value.ToString(),
_ => null
};
}
}
// Define an interface for messages that target a specific entity
public interface IWithOrderId
{
OrderId OrderId { get; }
}
// Use strongly-typed IDs
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString();
}
Create extension methods that abstract the execution mode:
using Akka.Cluster.Hosting;
using Akka.Cluster.Sharding;
using Akka.Hosting;
public static class OrderActorHostingExtensions
{
/// <summary>
/// Adds OrderActor with support for both local and clustered modes.
/// </summary>
public static AkkaConfigurationBuilder WithOrderActor(
this AkkaConfigurationBuilder builder,
AkkaExecutionMode executionMode = AkkaExecutionMode.Clustered,
string? clusterRole = null)
{
if (executionMode == AkkaExecutionMode.LocalTest)
{
// Non-clustered mode: Use GenericChildPerEntityParent
builder.WithActors((system, registry, resolver) =>
{
var parent = system.ActorOf(
GenericChildPerEntityParent.CreateProps(
new OrderMessageExtractor(),
entityId => resolver.Props<OrderActor>(entityId)),
"orders");
registry.Register<OrderActor>(parent);
});
}
else
{
// Clustered mode: Use ShardRegion
builder.WithShardRegion<OrderActor>(
"orders",
(system, registry, resolver) =>
entityId => resolver.Props<OrderActor>(entityId),
new OrderMessageExtractor(),
new ShardOptions
{
StateStoreMode = StateStoreMode.DData,
Role = clusterRole
});
}
return builder;
}
}
Create a convenience method that registers all domain actors:
public static class DomainActorHostingExtensions
{
/// <summary>
/// Adds all order domain actors with sharding support.
/// </summary>
public static AkkaConfigurationBuilder WithOrderDomainActors(
this AkkaConfigurationBuilder builder,
AkkaExecutionMode executionMode = AkkaExecutionMode.Clustered,
string? clusterRole = null)
{
return builder
.WithOrderActor(executionMode, clusterRole)
.WithPaymentActor(executionMode, clusterRole)
.WithShipmentActor(executionMode, clusterRole)
.WithNotificationActor(); // Singleton, no sharding needed
}
}
Register the ActorSystem's Scheduler as an ITimeProvider for testable time-based logic:
public static class SharedAkkaHostingExtensions
{
public static IServiceCollection AddAkkaWithTimeProvider(
this IServiceCollection services,
Action<AkkaConfigurationBuilder, IServiceProvider> configure)
{
// Register ITimeProvider using the ActorSystem's Scheduler
services.AddSingleton<ITimeProvider>(sp =>
sp.GetRequiredService<ActorSystem>().Scheduler);
return services.ConfigureAkka((builder, sp) =>
{
configure(builder, sp);
});
}
}
// In your actor, inject ITimeProvider
public class SubscriptionActor : ReceiveActor
{
private readonly ITimeProvider _timeProvider;
public SubscriptionActor(ITimeProvider timeProvider)
{
_timeProvider = timeProvider;
// Use _timeProvider.GetUtcNow() instead of DateTime.UtcNow
// This allows tests to control time
}
}
For durable scheduled tasks that survive restarts, use akka-reminders:
using Akka.Reminders;
using Akka.Reminders.Sql;
using Akka.Reminders.Sql.Configuration;
using Akka.Reminders.Storage;
public static class ReminderHostingExtensions
{
/// <summary>
/// Configures akka-reminders with PostgreSQL storage.
/// </summary>
public static AkkaConfigurationBuilder WithPostgresReminders(
this AkkaConfigurationBuilder builder,
string connectionString,
string schemaName = "reminders",
string tableName = "scheduled_reminders",
bool autoInitialize = true)
{
return builder.WithLocalReminders(reminders => reminders
.WithResolver(sys => new GenericChildPerEntityResolver(sys))
.WithStorage(system =>
{
var settings = SqlReminderStorageSettings.CreatePostgreSql(
connectionString,
schemaName,
tableName,
autoInitialize);
return new SqlReminderStorage(settings, system);
})
.WithSettings(new ReminderSettings
{
MaxSlippage = TimeSpan.FromSeconds(30),
MaxDeliveryAttempts = 3,
RetryBackoffBase = TimeSpan.FromSeconds(10)
}));
}
/// <summary>
/// Configures akka-reminders with in-memory storage for testing.
/// </summary>
public static AkkaConfigurationBuilder WithInMemoryReminders(
this AkkaConfigurationBuilder builder)
{
return builder.WithLocalReminders(reminders => reminders
.WithResolver(sys => new GenericChildPerEntityResolver(sys))
.WithStorage(system => new InMemoryReminderStorage())
.WithSettings(new ReminderSettings
{
MaxSlippage = TimeSpan.FromSeconds(1),
MaxDeliveryAttempts = 3,
RetryBackoffBase = TimeSpan.FromMilliseconds(100)
}));
}
}
Route reminder callbacks to GenericChildPerEntityParent actors:
using Akka.Actor;
using Akka.Hosting;
using Akka.Reminders;
/// <summary>
/// Resolves reminder targets to GenericChildPerEntityParent actors.
/// </summary>
public sealed class GenericChildPerEntityResolver : IReminderActorResolver
{
private readonly ActorSystem _system;
public GenericChildPerEntityResolver(ActorSystem system)
{
_system = system;
}
public IActorRef ResolveActorRef(ReminderEntry entry)
{
var registry = ActorRegistry.For(_system);
return entry.Key switch
{
var k when k.StartsWith("order-") =>
registry.Get<OrderActor>(),
var k when k.StartsWith("subscription-") =>
registry.Get<SubscriptionActor>(),
_ => throw new InvalidOperationException(
$"Unknown reminder key format: {entry.Key}")
};
}
}
For actors that should only have one instance:
public static AkkaConfigurationBuilder WithEmailSenderActor(
this AkkaConfigurationBuilder builder)
{
return builder.WithActors((system, registry, resolver) =>
{
var actor = system.ActorOf(
resolver.Props<EmailSenderActor>(),
"email-sender");
registry.Register<EmailSenderActor>(actor);
});
}
When you need to reference actors that are registered as parents:
/// <summary>
/// Marker type for ActorRegistry to retrieve the order manager
/// (GenericChildPerEntityParent for OrderActors).
/// </summary>
public sealed class OrderManagerActor;
// Usage in extension method
registry.Register<OrderManagerActor>(parent);
// Usage in controller/service
public class OrderService
{
private readonly IActorRef _orderManager;
public OrderService(IRequiredActor<OrderManagerActor> orderManager)
{
_orderManager = orderManager.ActorRef;
}
public async Task<OrderResponse> CreateOrder(CreateOrderCommand cmd)
{
return await _orderManager.Ask<OrderResponse>(cmd);
}
}
Actors don't have automatic DI scopes. Unlike ASP.NET controllers (where each HTTP request creates a scope), actors are long-lived. If you need scoped services (like DbContext), inject IServiceProvider and create scopes manually.
public sealed class OrderProcessingActor : ReceiveActor
{
private readonly IServiceProvider _serviceProvider;
private readonly IActorRef _notificationActor;
public OrderProcessingActor(
IServiceProvider serviceProvider,
IRequiredActor<NotificationActor> notificationActor)
{
_serviceProvider = serviceProvider;
_notificationActor = notificationActor.ActorRef;
ReceiveAsync<ProcessOrder>(HandleProcessOrder);
}
private async Task HandleProcessOrder(ProcessOrder msg)
{
// Create scope for this message - disposed after processing
using var scope = _serviceProvider.CreateScope();
// Resolve scoped services within the scope
var orderRepository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
var paymentService = scope.ServiceProvider.GetRequiredService<IPaymentService>();
var emailComposer = scope.ServiceProvider.GetRequiredService<IOrderEmailComposer>();
// Do work with scoped services
var order = await orderRepository.GetByIdAsync(msg.OrderId);
var payment = await paymentService.ProcessAsync(order);
// DbContext changes committed when scope disposes
}
}
| Benefit | Explanation |
|---|---|
| Fresh DbContext per message | No stale entity tracking between messages |
| Proper disposal | Database connections released after each message |
| Isolation | One message's errors don't corrupt another's state |
| Testable | Can inject mock IServiceProvider in tests |
For stateless, thread-safe services, inject directly (no scope needed):
public sealed class NotificationActor : ReceiveActor
{
private readonly IEmailLinkGenerator _linkGenerator; // Singleton - OK!
private readonly IMjmlTemplateRenderer _renderer; // Singleton - OK!
public NotificationActor(
IEmailLinkGenerator linkGenerator,
IMjmlTemplateRenderer renderer)
{
_linkGenerator = linkGenerator;
_renderer = renderer;
Receive<SendWelcomeEmail>(Handle);
}
}
// BAD: Scoped service injected into long-lived actor
public sealed class BadActor : ReceiveActor
{
private readonly IOrderRepository _repo; // Scoped! DbContext lives forever!
public BadActor(IOrderRepository repo) // Captured at actor creation
{
_repo = repo; // This DbContext will become stale
}
}
// GOOD: Inject IServiceProvider, create scope per message
public sealed class GoodActor : ReceiveActor
{
private readonly IServiceProvider _sp;
public GoodActor(IServiceProvider sp)
{
_sp = sp;
ReceiveAsync<ProcessOrder>(async msg =>
{
using var scope = _sp.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
// Fresh DbContext for this message
});
}
}
For more on DI lifetimes and scope management, see microsoft-extensions/dependency-injection skill.
RememberEntities controls whether the shard region remembers and automatically restarts all entities that were ever created. This should almost always befalse.
builder.WithShardRegion<OrderActor>(
"orders",
(system, registry, resolver) => entityId => resolver.Props<OrderActor>(entityId),
new OrderMessageExtractor(),
new ShardOptions
{
StateStoreMode = StateStoreMode.DData,
RememberEntities = false, // DEFAULT - almost always correct
Role = clusterRole
});
WhenRememberEntities = true causes problems:
| Problem | Explanation |
|---|---|
| Unbounded memory growth | Every entity ever created gets remembered and restarted forever |
| Slow cluster startup | Cluster must restart thousands/millions of entities on boot |
| Stale entity resurrection | Expired sessions, sent emails, old orders all get restarted |
| No passivation | Idle entities consume memory indefinitely (passivation is disabled) |
| Entity Type | RememberEntities | Reason |
|---|---|---|
UserSessionActor | false | Sessions expire, created on login |
DraftActor | false | Drafts are sent/discarded, ephemeral |
EmailSenderActor | false | Fire-and-forget operations |
OrderActor | false | Orders complete, new ones created constantly |
ShoppingCartActor |
Rule of thumb: Use RememberEntities = true only for:
When using WithShardRegion<T>, the generic parameter T serves as a marker type for the ActorRegistry. Use a dedicated marker type (not the actor class itself) for consistent registry access:
/// <summary>
/// Marker type for ActorRegistry. Use this to retrieve the OrderActor shard region.
/// </summary>
public sealed class OrderActorRegion;
// Registration - use marker type as generic parameter
builder.WithShardRegion<OrderActorRegion>(
"orders",
(system, registry, resolver) => entityId => resolver.Props<OrderActor>(entityId),
new OrderMessageExtractor(),
new ShardOptions { StateStoreMode = StateStoreMode.DData });
// Retrieval - same marker type
var orderRegion = ActorRegistry.Get<OrderActorRegion>();
orderRegion.Tell(new CreateOrder(orderId, amount));
Why marker types?
WithShardRegion<T> auto-registers the shard region under type TWithShardRegion<T> automatically registers the shard region in the ActorRegistry. Don't call registry.Register<T>() again:
// BAD - redundant registration
builder.WithShardRegion<OrderActorRegion>("orders", ...)
.WithActors((system, registry, resolver) =>
{
var region = registry.Get<OrderActorRegion>();
registry.Register<OrderActorRegion>(region); // UNNECESSARY!
});
// GOOD - WithShardRegion already registers
builder.WithShardRegion<OrderActorRegion>("orders", ...);
// That's it - OrderActorRegion is now in the registry
OrderId instead of string or GuidIWithOrderId for type-safe extractionDateTime.Now directly in actorsWeekly Installs
74
Repository
GitHub Stars
488
First Seen
Jan 28, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
claude-code57
codex47
opencode46
gemini-cli44
github-copilot44
kimi-cli41
Megatron-Core 大规模 LLM 训练指南:从 2B 到 462B 参数模型并行训练与优化
234 周安装
道德黑客方法论:完整渗透测试生命周期指南(侦察、扫描、漏洞分析、报告)
200 周安装
API 契约审计员 - 自动化审计 API 契约、服务边界与 DTO 模式的开发者工具
204 周安装
手动测试员技能指南:创建可执行测试脚本与回归测试流程
199 周安装
项目代码质量工具自动配置协调器 - 支持TypeScript、.NET、Python
201 周安装
Google官方SEO指南:提升搜索排名、结构化数据与Search Console监控最佳实践
198 周安装
线性任务规划器(协调器)ln-300-task-coordinator:AI驱动的任务分解与自动化协调工具
212 周安装
| false |
| Carts expire, abandoned carts common |
TenantActor | maybe true | Fixed set of tenants, always needed |
AccountActor | maybe true | Bounded set of accounts, long-lived |