OrchestrationTrigger函数中BlobClient调用后DbContext被释放问题
.NET 6 Durable Function 编排器中DbContext被释放问题分析与解决
问题背景
基于.NET 6.0独立模式搭建的Durable Function:
- ServiceBus触发函数
SampleWorker通过DurableClient调用编排函数PrepareStuff - 编排函数通过构造函数注入
IMyRepository,Program.cs中以Scoped生命周期注册MyDbContext和IMyRepository
异常现象
在编排函数PrepareStuff中:
- 首次调用
_repository.GetUser(1)执行成功 - 调用
BlobClient.ExistsAsync()后,第二次调用_repository.GetUser(1)抛出异常:Cannot access a disposed context instance...,调试确认此时DbContext已被释放
已尝试的方案
- 本地、Azure测试/生产环境均能复现问题
- 将
Repository或DbContext改为Singleton可解决问题,但不符合数据访问层最佳实践 - 将
Repository改为Transient无效果 - 将代码移到触发函数的
Run方法中执行正常 - 更新:将代码移到Activity函数后执行正常,疑问:是否因编排器不适合执行IO操作,导致对象生命周期被干扰?
问题原因
Durable Function的编排器函数采用重入式执行模型:当编排器遇到异步IO操作(如BlobClient.ExistsAsync())时,会生成持久化检查点并暂停执行,此时当前DI作用域会被完全释放,包括其中Scoped生命周期的DbContext。
当编排器从检查点恢复执行时,会创建新的DI作用域,但构造注入的IMyRepository仍持有旧作用域中已被释放的DbContext引用,因此第二次调用数据库方法时会触发已释放实例的异常。
同时,Durable Function官方设计规范明确要求:编排器函数不应该直接执行IO操作,IO操作必须放在Activity函数中执行——这也是代码移到Activity后恢复正常的核心原因:Activity函数拥有独立的DI作用域,每次执行都会创建全新的作用域,且不会因检查点机制导致作用域提前释放。
解决方案
推荐方案:遵循Durable Function设计规范
将所有IO操作(数据库访问、Blob存储操作、API调用等)迁移到Activity函数中执行,编排器仅负责流程编排逻辑:
- 编排器调用Activity函数执行
GetUser操作 - 编排器调用另一个Activity函数执行Blob检查操作
- 编排器根据各Activity的返回结果控制流程走向
这种方式完全符合Durable Function的设计原则,既避免了作用域释放的问题,也保证了编排器的重入安全性。
临时方案(不推荐生产环境使用)
如果因特殊需求必须在编排器中访问数据库,可以在每次需要访问时,手动创建新的DI作用域并获取实例:
// 构造注入IServiceProvider private readonly IServiceProvider _serviceProvider; public PrepareStuff(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } [FunctionName("PrepareStuff")] public async Task RunOrchestrator([OrchestrationTrigger] IDurableOrchestrationContext context) { // 第一次获取数据,创建独立作用域 using var scope1 = _serviceProvider.CreateScope(); var repository1 = scope1.ServiceProvider.GetRequiredService<IMyRepository>(); var user1 = await repository1.GetUser(1); // 执行Blob操作 var blobClient = new BlobClient("connectionString", "container", "blobName"); var exists = await blobClient.ExistsAsync(); // 第二次获取数据,重新创建作用域 using var scope2 = _serviceProvider.CreateScope(); var repository2 = scope2.ServiceProvider.GetRequiredService<IMyRepository>(); var user2 = await repository2.GetUser(1); }
注意:这种方式会绕过编排器的重入安全机制,可能导致重复执行等问题,仅建议在测试场景临时使用。
内容的提问来源于stack exchange,提问作者Aaron
相关产品推荐
相关产品推荐

