You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多Autofac容器模块化单体项目中如何正确注入IHubContext

多DI容器模块化单体应用中正确注入IHubContext实现SignalR消息发送

问题背景

我正在开发一个模块化单体应用,参考kgrzybek的modular monolith with ddd项目实现:每个模块拥有独立的Autofac IContainer,通过静态CompositionRoot类管理容器,以此简化作用域创建并避免模块间接口重复。

核心代码示例

  1. 模块容器管理类:
public static class ChatCompositionRoot
{
    private static IContainer _container = default!;

    public static void SetContainer(IContainer container)
    {
        _container = container;
    }

    public static ILifetimeScope BeginLifetimeScope()
    {
        return _container.BeginLifetimeScope();
    }
}
  1. 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)
        );
}
  1. 模块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 ْ

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 01:28:11