如何解决配置基于Entity Framework的Microsoft ConversationBot自定义存储的DI异常?
解决Teams通知Bot中EF存储的ObjectDisposedException异常
问题原因
你排查的结论完全正确:NotificationMiddleware是单例生命周期,而你注册的IConversationReferenceStore是Scoped生命周期。当Scoped的存储实例被单例中间件持有后,第一个请求结束时EF上下文会被DI容器自动释放,但中间件仍保留着这个已释放的存储实例,后续请求调用存储方法时就会触发System.ObjectDisposedException。
解决方案
核心思路是避免单例服务直接持有Scoped实例,改为每次需要使用时动态获取新的Scoped实例,以下是两种可行方案:
方案1:通过服务工厂延迟获取Scoped存储实例
这种方式既保留EF上下文的Scoped生命周期(避免并发问题),又解决单例中间件持有过期实例的问题。
- 创建存储工厂类
public class ConversationReferenceStoreFactory { private readonly IServiceProvider _serviceProvider; public ConversationReferenceStoreFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public IConversationReferenceStore GetScopedStore() { using var scope = _serviceProvider.CreateScope(); return scope.ServiceProvider.GetRequiredService<IConversationReferenceStore>(); } }
- 修改DI注册逻辑
builder.ConfigureServices(services => { // 保持Scoped存储注册不变 services.AddScoped<IConversationReferenceStore, ConversationReferenceStore>(); // 注册工厂为单例 services.AddSingleton<ConversationReferenceStoreFactory>(); // 原有Bot相关注册代码... // 调整ConversationBot的注册,使用工厂包装的存储 services.AddScoped(sp => { var storeFactory = sp.GetRequiredService<ConversationReferenceStoreFactory>(); var options = new ConversationOptions() { Adapter = sp.GetRequiredService<CloudAdapter>(), Notification = new NotificationOptions { BotAppId = botId, Store = new LazyScopedConversationStore(storeFactory) } }; return new ConversationBot(options); }); });
- 实现包装类适配接口
public class LazyScopedConversationStore : IConversationReferenceStore { private readonly ConversationReferenceStoreFactory _factory; public LazyScopedConversationStore(ConversationReferenceStoreFactory factory) { _factory = factory; } public async Task<bool> Add(string key, ConversationReference reference, ConversationReferenceStoreAddOptions options, CancellationToken cancellationToken) { var store = _factory.GetScopedStore(); return await store.Add(key, reference, options, cancellationToken); } // 实现IConversationReferenceStore的其他方法,逻辑同上 public async Task<IEnumerable<ConversationReference>> ListAsync(string channelId, CancellationToken cancellationToken) { var store = _factory.GetScopedStore(); return await store.ListAsync(channelId, cancellationToken); } // 省略其他接口方法实现... }
方案2:将EF上下文改为Singleton(不推荐)
可以临时将MyDbContext注册为Singleton,让上下文生命周期与单例中间件一致,但这会导致EF上下文在应用全程共享,可能引发并发冲突、内存泄漏等问题,仅适合测试场景,禁止生产环境使用:
services.AddDbContext<MyDbContext>(options => options.UseSqlServer("your_connection_string"), ServiceLifetime.Singleton);
关键注意事项
- 单例服务绝对不能持有Scoped/Transient实例,这是DI生命周期管理的核心规则。
- 使用
IServiceProvider.CreateScope()获取Scoped实例时,必须用using包裹Scope,确保实例使用后被正确释放。
内容的提问来源于stack exchange,提问作者Mark Nadig
相关产品推荐
相关产品推荐

