多Autofac容器模块化单体项目中如何正确注入IHubContext
多DI容器模块化单体应用中正确注入IHubContext实现SignalR消息发送
问题背景
我正在开发一个模块化单体应用,参考kgrzybek的modular monolith with ddd项目实现:每个模块拥有独立的Autofac IContainer,通过静态CompositionRoot类管理容器,以此简化作用域创建并避免模块间接口重复。
核心代码示例
- 模块容器管理类:
public static class ChatCompositionRoot { private static IContainer _container = default!; public static void SetContainer(IContainer container) { _container = container; } public static ILifetimeScope BeginLifetimeScope() { return _container.BeginLifetimeScope(); } }
- Program.cs中的公共服务注册:
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()); var config = builder.Configuration; var connectionString = config.GetConnectionString("PostgresConnection")!; var environment = builder.Environment; builder.Services.AddFeedModule(); builder.Services.AddUserModule(); builder.Services.AddChatModule(); builder.Services.AddApi(config); builder.Services.AddSignalR(); var app = builder.Build(); if (!environment.IsEnvironment("Testing")) { await Task.WhenAll( UsersStartup.InitializeAsync(builder.Services, connectionString, environment), FeedStartup.InitializeAsync(builder.Services, connectionString, environment), ChatStartup.InitializeAsync(builder.Services, connectionString, environment) ); }
- 模块Startup初始化逻辑:
public static class ChatStartup { public static async Task InitializeAsync( IServiceCollection serviceCollection, string connectionString, IWebHostEnvironment env) { ConfigureCompositionRoot(serviceCollection, connectionString); await PersistenceStartup.InitializeAsync(env); } private static void ConfigureCompositionRoot(IServiceCollection serviceCollection, string connectionString) { var containerBuilder = new ContainerBuilder(); containerBuilder.Populate(serviceCollection); containerBuilder.RegisterModule(new PersistenceModule(connectionString)); //// Module registeration var container = containerBuilder.Build(); ChatCompositionRoot.SetContainer(container); } }
问题现象
SignalR服务已注册到组合根中,但在服务层注入IHubContext<ChatHub, IChatHub>后,虽能获取实例却无法向客户端发送消息:
public class CreateDirectMessageCommandHandler( ChatDbContext context, IHubContext<ChatHub, IChatHub> hub ) : ICommandHandler<CreateDirectMessageCommand, SendDirectMessageResponse> { private readonly ChatDbContext _context = context; private readonly IHubContext<ChatHub, IChatHub> _hub = hub; public async Task<HandlerResponse<SendDirectMessageResponse>> ExecuteAsync(CreateDirectMessageCommand command, CancellationToken ct) { var chat = await _context.DirectChats.FindAsync([command.DirectChatId], ct); //logic var senderId = new ChatterId(command.UserId); var message = chat.AddMessage(senderId, command.Text); var recieverId = chat.GetRecieverId(senderId); // 无异常抛出,但消息无法发送到客户端 await _hub.Clients .User(recieverId) .SendDirectMessage(message.Id, message.Text); return (message.MapToResponse(), HandlerResponseStatus.Created); } }
解决方案
1. 核心问题分析
SignalR的IHubContext是注册在ASP.NET Core主服务容器中的,而当前每个模块都独立构建了自己的Autofac容器。通过containerBuilder.Populate(serviceCollection)复制的只是主容器构建前的服务快照,模块容器与主容器是完全隔离的实例。你注入的IHubContext是模块容器中的副本,和实际运行SignalR的主容器实例不共享连接映射,因此无法发送消息。
2. 推荐方案:共享主容器的子作用域
放弃每个模块构建独立完整容器的做法,改为让模块使用主容器的子作用域,确保依赖的服务(包括IHubContext)与主运行时一致:
步骤1:修改模块CompositionRoot
将容器管理改为基于主容器的子作用域:
public static class ChatCompositionRoot { private static ILifetimeScope _rootScope = default!; private static string _connectionString = default!; public static void Initialize(ILifetimeScope rootScope, string connectionString) { _rootScope = rootScope; _connectionString = connectionString; } public static ILifetimeScope BeginLifetimeScope() { return _rootScope.BeginLifetimeScope(builder => { // 在这里注册模块专属服务,替代原Autofac Module builder.RegisterModule(new PersistenceModule(_connectionString)); // 其他模块服务注册逻辑 }); } }
步骤2:调整Program.cs逻辑
构建主容器后,将其引用传递给各模块:
var app = builder.Build(); // 获取主Autofac容器的根作用域 var rootLifetimeScope = app.Services.GetRequiredService<ILifetimeScope>(); // 初始化各模块的CompositionRoot ChatCompositionRoot.Initialize(rootLifetimeScope, connectionString); UserCompositionRoot.Initialize(rootLifetimeScope, connectionString); FeedCompositionRoot.Initialize(rootLifetimeScope, connectionString); if (!environment.IsEnvironment("Testing")) { await Task.WhenAll( UsersStartup.InitializeAsync(environment), FeedStartup.InitializeAsync(environment), ChatStartup.InitializeAsync(environment) ); }
步骤3:简化模块Startup
不再需要模块独立构建容器:
public static class ChatStartup { public static async Task InitializeAsync(IWebHostEnvironment env) { await PersistenceStartup.InitializeAsync(env); } }
3. 备选方案:跨容器共享IHubContext单例
若坚持保留模块独立容器,需确保所有模块使用的IHubContext是同一个实例:
步骤1:在Program.cs中提取IHubContext实例
builder.Services.AddSignalR(); // 构建临时服务提供者获取全局IHubContext实例 var tempServiceProvider = builder.Services.BuildServiceProvider(); var chatHubContext = tempServiceProvider.GetRequiredService<IHubContext<ChatHub, IChatHub>>();
步骤2:在模块容器中注册单例实例
修改模块Startup的ConfigureCompositionRoot方法:
private static void ConfigureCompositionRoot(IServiceCollection serviceCollection, string connectionString, IHubContext<ChatHub, IChatHub> chatHubContext) { var containerBuilder = new ContainerBuilder(); containerBuilder.Populate(serviceCollection); // 注册全局共享的IHubContext单例 containerBuilder.RegisterInstance(chatHubContext) .As<IHubContext<ChatHub, IChatHub>>() .SingleInstance(); containerBuilder.RegisterModule(new PersistenceModule(connectionString)); // 其他模块服务注册 var container = containerBuilder.Build(); ChatCompositionRoot.SetContainer(container); }
4. 额外检查项
- 确认
recieverId的格式与SignalR用户标识完全匹配(比如是否为字符串、是否和认证系统中的UserId一致) - 检查客户端是否正确监听了
SendDirectMessage方法,无拼写或参数类型错误 - 确保SignalR Hub已正确映射路由(如
app.MapHub<ChatHub>("/chat"))
内容的提问来源于stack exchange,提问作者SY7K ْ
相关产品推荐
相关产品推荐

