ReqNroll集成测试中HangFire实例销毁异常问题求助
问题:连续运行ReqNroll集成测试时HangFire InMemory调度器已销毁报错
问题现象
单个测试运行正常,连续执行所有测试时仅第一个成功,其余均触发内部服务器错误,抛出ObjectDisposedException,提示访问已销毁的HangFire调度器对象。
错误详情
[2024-08-30 09:35:55] fail: TestProject.Api.Filters.GlobalApiExceptionFilter[0] An exception occurred while processing the request. Hangfire.BackgroundJobClientException: Background job creation failed. See inner exception for details. ---> System.ObjectDisposedException: Cannot access a disposed object. Object name: 'Hangfire.InMemory.State.Dispatcher`1[[System.UInt64, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken={}]]'. at Hangfire.InMemory.State.Dispatcher`1.ThrowObjectDisposedException() in /_/src/Hangfire.InMemory/State/Dispatcher.cs:line 170 at Hangfire.InMemory.State.Dispatcher`1.QueryWriteAndWait(ICommand`2 query) in /_/src/Hangfire.InMemory/State/Dispatcher.cs:line 68 at Hangfire.InMemory.State.DispatcherBase`1.QueryWriteAndWait[T](ICommand`2 query) in /_/src/Hangfire.InMemory/State/DispatcherBase.cs:line 168 at Hangfire.InMemory.InMemoryConnection`1.CreateExpiredJob(Job job, IDictionary`2 parameters, DateTime createdAt, TimeSpan expireIn) in /_/src/Hangfire.InMemory/InMemoryConnection.cs:line 112 at Hangfire.Client.CoreBackgroundJobFactory.<>c__DisplayClass15_0.<CreateBackgroundJobTwoSteps>b__0(Int32 _) at Hangfire.Client.CoreBackgroundJobFactory.RetryOnException[T](Int32& attemptsLeft, Func`2 action) --- End of stack trace from previous location ---
测试环境与代码
使用ReqNroll + NUnit,基于WebApplicationFactory配置外部服务桩、内存数据库和HangFire InMemory实例。
Setup与CleanUp代码
[BeforeScenario] public void Setup() { _httpClient = _factory.WithWebHostBuilder(builder => { builder.ConfigureTestServices(services => { services .ConfigureExternalConnectionStubs() .ConfigureLocalDb() .ConfigureHangfire(); }); }).CreateClient(); _potentialThreatRequests = []; _statusCodes = []; _httpContents = []; _cachedThreatResponses = []; _cachedProblemDetails = []; }
[AfterScenario] public void CleanUp() { _httpClient.Dispose(); }
已在ConfigureLocalDb和ConfigureHangfire中移除旧服务并重新注册内存版HangFire,但问题仍存在,且仅在连续执行文件上传类测试步骤时触发。已尝试销毁HttpClient、重新初始化JobStorage,无效。使用版本:Hangfire 1.8.5、Hangfire.InMemory 0.10.1、ReqNroll 2.0.3。
原因分析
- HangFire InMemory实例生命周期冲突:
WebApplicationFactory默认复用宿主实例,第一次测试结束后HangFire调度器被销毁,但后续测试复用了包含已销毁对象的服务容器,导致访问已释放资源。 - 测试隔离不彻底:当前Setup仅重新配置服务,未彻底重置宿主环境,前一次测试的HangFire状态(含已销毁调度器)残留。
- HttpClient与宿主绑定关系未切断:虽每次测试创建新HttpClient,但
WithWebHostBuilder未强制创建全新宿主,宿主内的HangFire服务已被前一次测试的销毁流程标记为已释放。
解决方案
1. 每次测试创建全新WebApplicationFactory实例
不再复用全局_factory,每个测试的Setup中创建新工厂实例,确保服务容器和HangFire实例完全独立:
private WebApplicationFactory<Program> _factory; [BeforeScenario] public void Setup() { _factory = new WebApplicationFactory<Program>(); _httpClient = _factory.WithWebHostBuilder(builder => { builder.ConfigureTestServices(services => { services .ConfigureExternalConnectionStubs() .ConfigureLocalDb() .ConfigureHangfire(); }); }).CreateClient(); // 其他初始化代码... } [AfterScenario] public void CleanUp() { _httpClient.Dispose(); _factory.Dispose(); // 销毁工厂,彻底释放宿主资源 }
2. 彻底重置HangFire服务与状态
在ConfigureHangfire中,不仅移除旧服务,还要销毁旧JobStorage实例,强制创建全新InMemory存储:
public static IServiceCollection ConfigureHangfire(this IServiceCollection services) { // 移除所有HangFire相关服务描述 var hangfireDescriptors = services.Where(d => d.ServiceType == typeof(IJobStorage) || d.ServiceType == typeof(IBackgroundJobClient) || d.ServiceType == typeof(IRecurringJobManager)).ToList(); foreach (var descriptor in hangfireDescriptors) { services.Remove(descriptor); } // 销毁旧的JobStorage实例 if (JobStorage.Current is IDisposable disposableStorage) { disposableStorage.Dispose(); } // 注册全新的InMemory HangFire服务 services.AddHangfire(config => config.UseInMemoryStorage()); services.AddHangfireServer(); return services; }
3. 禁用WebApplicationFactory宿主复用
通过配置工厂选项,强制每次测试创建全新宿主:
_factory = new WebApplicationFactory<Program>() { Options = new WebApplicationFactoryOptions { AllowHostReuse = false } };
4. 测试后主动清理HangFire后台服务
在AfterScenario中获取并销毁HangFire后台服务器实例,确保资源完全释放:
[AfterScenario] public void CleanUp() { _httpClient.Dispose(); // 停止并销毁HangFire后台服务 var hangfireServer = _factory.Services.GetService<IHostedService>() as BackgroundJobServer; hangfireServer?.StopAsync(CancellationToken.None).Wait(); hangfireServer?.Dispose(); _factory.Dispose(); }
内容的提问来源于stack exchange,提问作者Jan Pul
相关产品推荐
相关产品推荐

