- 后端
- 微服务
【免费下载链接】akka.net
Canonical actor model implementation for .NET with local + distributed actors in C# and F#.
本文基于 Akka.NET 仓库中Akka.Hosting与Akka.DependencyInjection两个模块的真实实现,系统讲解 Akka.NET 托管层提供的两个核心依赖注入 API——ActorRegistry与IRequiredActor<TKey>:前者是一个以类型(Type)为键、IActorRef为值的“Actor 服务定位器”,后者则让你把任意已注册的顶层 Actor 通过构造函数注入到 ASP.NET、SignalR、gRPC 乃至其他 Actor 中。读完后,你将掌握如何在Microsoft.Extensions.Hosting宿主中注册 Actor、如何在 Actor 系统内外解析IActorRef,以及如何通过IDependencyResolver在集群组件(如 ShardRegion)内部使用 DI。
两个 API 的设计目标
Akka.Hosting 的设计目标之一,是让 Akka.NET 的依赖注入体验与其他 .NET 技术一样无缝。它通过两个新 API 实现这一点:
ActorRegistry:一个以Type为键、IActorRef为值设计的 DI 容器,定位类似于IServiceCollection之于 ASP.NET 服务的角色;IRequiredActor<TKey>:把它声明在任何依赖注入资源的构造函数参数上,就会自动从ActorRegistry中解析出以TKey为键存储的 Actor 引用。这正是把 Actor 注入到 ASP.NET、SignalR、gRPC 以及其他 Akka.NET Actor 的通道。
注意:
ActorRegistry与ActorSystem都会自动注册到应用关联的IServiceCollection/IServiceProvider上。
这一“自动注册”并非口号。从 AkkaConfigurationBuilder.Bind() 的实现可以看到,AddAkka(...)被调用时会一次性完成以下绑定:
internal void Bind() { // register as singleton - not interested in supporting multi-Sys use cases ServiceCollection.AddSingleton(ActorSystemFactory()); ServiceCollection.AddSingleton<ActorRegistry>(sp => { return ActorRegistry.For(sp.GetRequiredService<ActorSystem>()); }); ServiceCollection.AddSingleton<IActorRegistry>(sp => sp.GetRequiredService<ActorRegistry>()); ServiceCollection.AddSingleton<IReadOnlyActorRegistry>(sp => sp.GetRequiredService<ActorRegistry>()); ServiceCollection.AddSingleton(typeof(IRequiredActor<>), typeof(RequiredActor<>)); // ... 健康检查注册 }也就是说,只要调用了AddAkka,ActorSystem、ActorRegistry(及其两个只读接口形式)和开放泛型注册IRequiredActor<>→RequiredActor<>就都进入了 DI 容器。这也是后文“无需额外注册即可使用IRequiredActor<T>”的根源。
向 ActorRegistry 注册 Actor
在 Akka.Hosting 中,我们需要一种简单的方式,在ActorSystem内部和外部通过依赖注入传递顶层IActorRef。ActorRegistry通过一组泛型、强类型方法,让长生命周期IActorRef的存取变得简单而一致。共有三种典型获取/注册路径:
1. 从 ActorSystem 手动获取
var registry = ActorRegistry.For(myActorSystem);ActorRegistry.For的实现在 ActorRegistry.cs:
public static ActorRegistry For(ActorSystem actorSystem) { return actorSystem.WithExtension<ActorRegistry, ActorRegistryExtension>(); }它本质上是一个 Akka.NETExtension(扩展):ActorRegistry以单例形式挂载在ActorSystem上,因此同一个系统内多次调用For拿到的是同一个实例。
2. 通过 Actor 构建器注册
在AddAkka的配置委托中,WithActors会同时把ActorSystem和IActorRegistry传给你:
builder.Services.AddAkka("MyActorSystem", configurationBuilder => { configurationBuilder .WithActors((system, actorRegistry) => { var actor = system.ActorOf(Props.Create(() => new MyActor)); actorRegistry.TryRegister<MyActor>(actor); // 注册到 DI }); });从 AkkaHostingExtensions.WithActors 可以看到,WithActors提供了多个重载,委托签名分别为Action<ActorSystem, IActorRegistry>、Action<ActorSystem, IActorRegistry, IDependencyResolver>以及异步的ActorStarter/ActorStarterWithResolver委托。这些 starter 会被收集到AkkaConfigurationBuilder._actorStarters列表中,并在宿主启动时按顺序执行。
3. 手动获取 IActorRef
var registry = ActorRegistry.For(myActorSystem); registry.Get<Index>(); // 用于 DIActorRegistry 的 API 全景
结合 ActorRegistry.cs 的接口定义,其核心 API 可以整理为三组:
| 接口 | 方法 | 语义 |
|---|---|---|
IReadOnlyActorRegistry | TryGet<TKey>(out IActorRef) | 尝试按键取引用;未命中时输出ActorRefs.Nobody |
Get<TKey>() | 按键取引用;未注册时抛出MissingActorRegistryEntryException | |
GetAsync<TKey>(CancellationToken) | 异步等待某个 Actor 注册完成(启动早期可用) | |
IActorRegistry | Register<TKey>(IActorRef, bool overwrite = false) | 注册;重复键且不允许覆盖时抛DuplicateActorRegistryException |
TryRegister<TKey>(IActorRef, bool overwrite = false) | 注册;失败返回false | |
IEnumerable<KeyValuePair<Type, IActorRef>> | 枚举 | 可遍历所有已注册的 Actor |
两个值得注意的实现细节:
- 线程安全:内部使用
ConcurrentDictionary<Type, IActorRef>存储注册项(见 ActorRegistry.cs),因此注册与解析可以安全地发生在不同线程/宿主服务上。 - 等待注册机制:
GetAsync内部使用TaskCompletionSource与WaitForActorRegistration集合实现“等 Actor 注册好再返回”的语义(见 GetAsync 实现)。当TryRegister成功时会调用NotifyWaiters唤醒所有等待者;取消令牌被触发时会把等待项从集合中移除并将 Task 置为已取消。
源码注释也明确划定了使用边界:ActorRegistry只应用于需要被系统内外访问的顶层 Actor——如果你把ActorSystem里每一个 Actor 都塞进注册表,那一定是用法错了(见 类文档注释)。
用 IRequiredActor 注入 Actor
假设有一个类依赖某个顶层 Actor、路由(Router)、ShardRegion,甚至是ClusterSingleton的引用(这些是经常需要与非 Akka.NET 部分交互的 Actor 类型):
public sealed class MyConsumer { private readonly IActorRef _actor; public MyConsumer(IRequiredActor<MyActorType> actor) { _actor = actor.ActorRef; } public async Task<string> Say(string word) { return await _actor.Ask<string>(word, TimeSpan.FromSeconds(3)); } }IRequiredActor<MyActorType>会让 Microsoft.Extensions.DependencyInjection 机制从ActorRegistry中解析出MyActorType对应的引用,并注入到传给MyConsumer的实例中。
接口与实现
文档中给出的接口形态为单一ActorRef属性;当前仓库中的完整定义在 ActorRegistry.cs,实际上还多了一个面向宿主启动时序的GetAsync:
public interface IRequiredActor<TActor> { /// <summary> /// The underlying actor resolved via <see cref="ActorRegistry"/> using the given <see cref="TActor"/> key. /// </summary> IActorRef ActorRef { get; } /// <summary> /// 在从另一个 IHostedService 内部调用时,Actor 注册在启动阶段可能尚不可用 /// (Akka.NET 本身是在另一个 hosted service 中异步启动的)。 /// 应调用 GetAsync 等待 AkkaService 在启动时把 Actor 填充进 ActorRegistry。 /// </summary> Task<IActorRef> GetAsync(CancellationToken cancellationToken = default); }其实现RequiredActor<TActor>(ActorRegistry.cs)采用三级查找策略:
- 命中本地缓存:首次解析成功后缓存到
_internalRef,后续零成本; - 同步查注册表(快路径):
_registry.TryGet<TActor>; - 失败处理:
ActorRef属性若仍取不到,抛出MissingActorRegistryEntryException,异常信息会明确提示“如果你是在IHostedService构造函数中使用IRequiredActor<T>,请改用GetAsync等待 Actor 被 AkkaService(并行运行中)填充”;而GetAsync则继续落到第 3 级——等待注册事件。
完整的最小可运行示例
默认情况下,任何已注册到ActorRegistry的 Actor 都可以被自动解析——不需要在IServiceCollection上做任何特殊声明:
using var host = new HostBuilder() .ConfigureServices(services => { services.AddAkka("MySys", (builder, provider) => { builder.WithActors((system, registry) => { var actor = system.ActorOf(Props.Create(() => new MyActorType()), "myactor"); registry.Register<MyActorType>(actor); }); }); services.AddScoped<MyConsumer>(); }) .Build(); await host.StartAsync();把 Actor 和类型键加入ActorRegistry就足够了——访问该类型的IRequiredActor<TActor>无需额外的 DI 注册。
仓库测试 RequiredActorSpecs.cs 覆盖了三个关键行为,可以当作验收清单:
ShouldRetrieveRequiredActorFromIServiceProvider:从host.Services解析MyConsumer,其内部IRequiredActor<MyActorType>成功拿到引用并完成 Ask 往返;ShouldFailRetrieveRequiredActorWhenNotDefined:注册表中不存在的键(MissingActor)在解析时抛出MissingActorRegistryEntryException;ShouldNotCacheNobody...两个用例:在host.StartAsync()之前调用ActorRef会抛异常而不是缓存Nobody;调用GetAsync()挂起等待,StartAsync完成注册后 Task 才完成,且缓存结果不是Nobody。
Native AOT 注意:类型键请使用 class,不要用 struct。
IRequiredActor<TKey>通过开放泛型 DI 注册解析,而在 Native AOT 下 Microsoft.Extensions.DependencyInjection 无法为值类型键构造该开放泛型,struct 键会在运行时失败。
启动时序:为什么 GetAsync 是必要的
AkkaHostedService(AkkaHostedService.cs)是AddAkka注册到宿主中的IHostedService,它在StartAsync中先解析单例ActorSystem,再执行ConfigurationBuilder.StartAsync(ActorSystem)——所有WithActors注册的 starter 就在这个阶段运行、把 Actor 填入ActorRegistry。由于宿主可能同时并行启动多个IHostedService,你的服务若在自己的StartAsync中同步访问ActorRef,存在“注册还没发生”的竞态窗口;GetAsync正是为此准备的阻塞点。
在 Akka.NET 内部解析 IRequiredActor
Akka.NET 默认不使用 DI 来启动 Actor,主要原因在于 Actor 的生命周期默认是无界的——这使得推断注入依赖的作用域变得不直观。相比之下 ASP.NET 就简单得多:所有 HTTP 请求都是请求作用域,所有 WebSocket 连接都是连接作用域,这些对象具有有界且通常较短的生命周期。
因此,用户需要显式声明“我要用 Microsoft.Extensions.DependencyInjection”,途径是 Akka.DependencyInjection 中的IDependencyResolver接口——而在 Akka.Hosting 大多数启动 Actor 的 API 中这都很方便:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddScoped<IReplyGenerator, DefaultReplyGenerator>(); builder.Services.AddAkka("MyActorSystem", configurationBuilder => { configurationBuilder .WithRemoting(hostname: "localhost", port: 8110) .WithClustering(new ClusterOptions{SeedNodes = new []{ "akka.tcp://MyActorSystem@localhost:8110", }}) .WithShardRegion<Echo>( typeName: "myRegion", entityPropsFactory: (_, _, resolver) => { // 使用 DI 把 IReplyGenerator 注入 EchoActor return s => resolver.Props<EchoActor>(s); }, extractEntityId: ExtractEntityId, extractShardId: ExtractShardId, shardOptions: new ShardOptions()); });resolver.Props<EchoActor>()会利用ActorSystem内置的IDependencyResolver实例化EchoActor,并注入全部所需依赖——包括IRequiredActor<TKey>,这意味着 Actor 内部也可以互相引用已注册的顶层 Actor。
底层调用链
这条链路在源码中是清晰可追的:
- 构建 ActorSystem 时注入 Setup。AkkaConfigurationBuilder.ActorSystemFactory() 在真正
ActorSystem.Create(...)之前,会执行var diSetup = DependencyResolverSetup.Create(sp);并把该 Setup 合并进BootstrapSetup。DependencyResolverSetup内部创建ServiceProviderDependencyResolver(包装宿主IServiceProvider)。 IDependencyResolver通过 Extension 暴露。DependencyResolver.cs 中,DependencyResolver.For(system)从system.Settings.Setup取出DependencyResolverSetup;如果系统构建时没有附带该 Setup,会抛出ConfigurationException并在事件流中发布错误。Props<T>走 DI 生产者。IDependencyResolver.Props 的文档明确警告:“你需要自行管理依赖的生命周期,Akka.NET 不会替你管理”。ServiceProviderDependencyResolver 的具体实现是Props.CreateBy(new ServiceProviderActorProducer<T>(ServiceProvider, args)),即由 DI 容器按构造函数签名逐个解析参数。WithActors带 resolver 的重载如何拿到它:AkkaConfigurationBuilder.ToAsyncStarter 在执行 starter 前调用DependencyResolver.For(f).Resolver,把 resolver 交给Action<ActorSystem, IActorRegistry, IDependencyResolver>委托——WithShardRegion等集群 API 正是基于这个机制工作的。
以WithShardRegion为例,其内部实现Resolverstarter 会先调用entityPropsFactory(system, registry, resolver),启动ShardRegion后执行registry.Register<TKey>(shardRegion)——ShardRegion 的IActorRef随即对IRequiredActor<TKey>可见,外部组件(Controller、SignalR Hub 等)就能直接注入它。
作用域(Scope)警告
DependencyResolver.Resolver属性上的文档注释(DependencyResolver.cs)给出了官方立场,值得原样理解:
请为你的 Actor 及其消费的依赖使用恰当的
IServiceScope。DI 通常不适用于长生命周期的有状态对象,例如 Actor。因此在大多数情况下,通过构造函数注入 transient 依赖是个坏主意——更好的做法是在 Actor 每次处理需要 transient 依赖(如数据库连接)的消息时创建一个局部“请求作用域”,操作完成后将其释放。Actor 不是 MVC Controller。Actor 可以永远存活、可以重启、经常有状态。请在使用该特性时保持警惕,否则会出大问题。Akka.NET 不会神奇地替你管理作用域。
仓库中的测试也印证了这套 DI 与 Actor 机制的组合能力:DiPropsSpecs.cs 验证了resolver.Props<T>(arg)返回的Props可以与.WithMailbox(...)等常规Props配置自由组合;ActorServiceProviderPropsWithScopesSpecs.cs 则覆盖了显式CreateScope()的用法。另外注意历史演进:旧版Akka.DependencyInjection.ServiceProvider扩展已在 v1.4.21 起标记为[Obsolete],官方推荐切换到DependencyResolver(见 ServiceProvider.cs)。
小结
围绕 docs/articles/hosting/dependency-injection.md 这条主线,可以把 Akka.NET 托管层的 DI 体系归纳为:
- 注册侧:
ActorRegistry是ActorSystem级的类型化引用表(ConcurrentDictionary+ 等待机制),通过ActorRegistry.For(system)、WithActorsstarter 或集群 API(WithShardRegion等自动Register)填充;它随AddAkka自动以单例形式注册到宿主 DI 容器。 - 消费侧:
IRequiredActor<TKey>经开放泛型注册自动可用,ActorRef属性提供同步快路径(未就绪则抛MissingActorRegistryEntryException),GetAsync提供启动期等待;类型键必须用 class 以兼容 Native AOT。 - Actor 内部:通过
IDependencyResolver.Props<T>()显式启用 DI 创建 Actor,从而在 Actor 内也能注入普通服务与IRequiredActor<TKey>;代价是你要自行管理依赖作用域与生命周期。 - 生命周期边界:
AkkaHostedService负责把ActorSystem的启动/关闭(CoordinatedShutdown)接入IHostedService生命周期,这也是整个 DI 时序(starter 何时运行、注册何时可见)的锚点。
- 后端
- 微服务
【免费下载链接】akka.net
Canonical actor model implementation for .NET with local + distributed actors in C# and F#.
相关推荐
Akka.NET 依赖注入实战指南:从 Akka.DependencyInjection 到 Actor 生命周期管理
Akka.NET 依赖注入实战指南:从 Akka.DependencyInjection 到 Actor 生命周期管理 本文围绕 Akka.NET 官方推荐的依
后端微服务Karmada 依赖解析:klog 内部 clock 包如何实现可注入时间接口与循环依赖规避
Karmada 依赖解析:klog 内部 clock 包如何实现可注入时间接口与循环依赖规避 导读 klog 是 Kubernetes 生态中最常用的 Go 日
云原生多集群集群管理微服务Dagger Android实战:如何在Android应用中优雅实现依赖注入
Dagger Android实战:如何在Android应用中优雅实现依赖注入 Dagger Android是Android开发中最强大的依赖注入框架,专门为An
后端代码生成依赖注入
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考