Discord.NET 依赖注入注册AddDbContext后DatabaseContext被共享的解决方法
问题根因定位
AddDbContext默认将DatabaseContext注册为 Scoped(作用域生命周期) 服务,设计上绑定单次任务的执行周期,任务完成后自动释放。- 当前代码在调用命令执行方法时,直接传入了根容器的全局
IServiceProvider。当没有为单次命令执行创建独立作用域时,从根容器解析Scoped服务会触发DI的默认行为:将该Scoped服务提升为根容器单例,导致所有命令请求共享同一个DatabaseContext实例,出现跨请求的实体缓存复用问题。 - Discord.NET 的CommandService本身不会自动为每次命令执行创建DI作用域,需要开发者手动管控作用域的创建与释放流程。
调整实现方案
- 现有服务注册逻辑不需要修改,
AddDbContext的默认Scoped注册策略完全匹配需求,无需调整为其他生命周期。 - 修改命令执行入口,为每一次用户触发的命令单独创建独立DI作用域,从作用域内解析服务传入命令执行流程。命令执行完成后,自动释放整个作用域,连带释放作用域内创建的DatabaseContext实例。
修改后的HandleCommandAsync代码如下:
private async Task HandleCommandAsync(SocketMessage messageParam) { var message = messageParam as SocketUserMessage; if (message == null) return; int argPos = 0; if (!(message.HasStringPrefix("b.", ref argPos) || message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || message.Author.IsBot) return; var context = new SocketCommandContext(_client, message); // 为单次命令执行创建独立DI作用域 using var commandScope = _services.CreateScope(); // 传入当前作用域的服务提供者执行命令 await _commands.ExecuteAsync(context, argPos, commandScope.ServiceProvider); }
- 现有
UserModule的实现不需要做任何调整。模块实例、注入的DatabaseContext都会从当前命令对应的独立作用域中解析,保证每一次命令执行拿到的都是全新的DatabaseContext实例,命令结束后随着作用域释放自动完成资源回收。
注意:不要为了实现独立实例的目标,把DatabaseContext改成Transient或Singleton生命周期。Scoped是EFCore官方推荐的数据库上下文生命周期,配合独立作用域可以自动完成实例创建、缓存隔离、资源释放的全流程管控,不会出现数据库连接泄漏或缓存不一致的问题。
效果验证
调整完成后,连续多次执行b.user命令时,打印的ContextId会依次变化、互不重复。DatabaseContext的生命周期完全和单次命令执行绑定,不会跨请求缓存实体数据,也就不会出现和其他共享数据库的应用之间的数据缓存不一致问题。
内容的提问来源于stack exchange,提问作者Et3rnos
相关产品推荐
相关产品推荐

