使用XUnit测试.NET 6 Web API时EF Core查询触发AggregateException问题
EF Core集成测试中Hosted Service停止异常问题解决
问题场景与现象
在使用XUnit对基于C# .NET 6和EF Core开发的Web API进行集成测试时,调试阶段请求可正常到达Web API及服务层,但执行EF Core上下文查询(例如如下代码)时,会在Program.cs处中断,并抛出异常:
private Message? GetMessage() => _myContext.Messages.OrderBy(m => m.CreatedUtc).FirstOrDefault();
异常信息:
System.AggregateException: 'One or more hosted services failed to stop. (The operation was canceled.)'
用户提供的测试相关代码如下:
TestingWebAppFactory类代码
public class TestingWebAppFactory<TEntryPoint> : WebApplicationFactory<Program> where TEntryPoint : Program { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var descriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<MyContext>)); if (descriptor != null) services.Remove(descriptor); services.AddDbContext<MyContext>(options => { options.UseInMemoryDatabase("myinmemorydb"); }); var sp = services.BuildServiceProvider(); using (var scope = sp.CreateScope()) using (var appContext = scope.ServiceProvider.GetRequiredService<MyContext>()) { try { appContext.Database.EnsureCreated(); } catch (Exception ex) { //Log errors or do anything you think it's needed throw; } } }); } }
XUnit测试类代码
public class MyServiceTest : IClassFixture<TestingWebAppFactory<Program>> { private readonly HttpClient _client; public MyServiceTest(TestingWebAppFactory<Program> factory) { _client = factory.CreateClient(); } [Fact] public async Task WhenAValidMessagePosted_ThenShouldReturn() { CancellationTokenSource source = new CancellationTokenSource(); CancellationToken token = source.Token; source.CancelAfter(TimeSpan.FromSeconds(5)); var response = await _client.GetAsync("https://localhost:xxxx/api/service/message/post?cronExpresson=0"); } }
问题原因分析
- 手动CancellationToken提前取消:测试中手动设置5秒后取消请求,会导致请求未完成就被终止,进而触发Hosted Service的强制停止逻辑,抛出取消异常。
- Hosted Service与测试生命周期冲突:Web API中的Hosted Service在测试结束时可能无法正常停止,与测试的资源释放逻辑产生冲突。
- InMemory数据库上下文污染:固定名称的InMemory数据库可能导致不同测试间的数据残留,引发上下文查询时的隐性错误。
解决方案
1. 移除测试中不必要的CancellationToken
测试中手动设置的CancelAfter属于冗余操作,HttpClient默认自带合理超时机制,手动取消会干扰请求正常执行。修改测试类代码:
public class MyServiceTest : IClassFixture<TestingWebAppFactory<Program>>, IDisposable { private readonly HttpClient _client; private readonly TestingWebAppFactory<Program> _factory; public MyServiceTest(TestingWebAppFactory<Program> factory) { _factory = factory; _client = factory.CreateClient(); } [Fact] public async Task WhenAValidMessagePosted_ThenShouldReturn() { // 使用默认超时,移除手动取消逻辑 var response = await _client.GetAsync("https://localhost:xxxx/api/service/message/post?cronExpresson=0"); // 添加必要断言,验证请求结果 response.EnsureSuccessStatusCode(); } public void Dispose() { // 显式释放测试资源 _client.Dispose(); _factory.Dispose(); } }
2. 调整TestingWebAppFactory的服务配置
如果测试不需要依赖Web API中的Hosted Service,可以直接移除相关服务,避免停止时的冲突;同时为InMemory数据库生成唯一名称,避免测试间数据污染:
public class TestingWebAppFactory<TEntryPoint> : WebApplicationFactory<Program> where TEntryPoint : Program { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { // 移除原有DbContext配置 var dbDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<MyContext>)); if (dbDescriptor != null) services.Remove(dbDescriptor); // 移除所有Hosted Service(测试无需依赖时) var hostedServiceDescriptors = services.Where(d => d.ServiceType == typeof(IHostedService)).ToList(); foreach (var desc in hostedServiceDescriptors) { services.Remove(desc); } // 配置唯一名称的InMemory数据库 services.AddDbContext<MyContext>(options => { options.UseInMemoryDatabase($"MyInMemoryDb_{Guid.NewGuid()}"); }); // 初始化数据库 var sp = services.BuildServiceProvider(); using (var scope = sp.CreateScope()) using (var appContext = scope.ServiceProvider.GetRequiredService<MyContext>()) { try { appContext.Database.EnsureCreated(); } catch (Exception ex) { // 可添加日志记录逻辑 throw; } } }); } }
3. 保留Hosted Service时的兼容处理
如果必须保留Hosted Service,需确保其停止逻辑与测试生命周期兼容:在测试结束前,显式停止Hosted Service,避免强制取消。例如在测试类的Dispose方法中添加相关逻辑:
public async void Dispose() { var host = _factory.Server.Host; var hostedServices = host.Services.GetServices<IHostedService>(); foreach (var service in hostedServices) { if (service is IAsyncDisposable asyncDisposable) { await asyncDisposable.DisposeAsync(); } else { (service as IDisposable)?.Dispose(); } } _client.Dispose(); _factory.Dispose(); }
生效说明
- 移除手动取消逻辑:避免请求提前终止,让Hosted Service有足够时间正常停止。
- 移除Hosted Service(按需):直接消除测试与后台服务的生命周期冲突。
- 唯一InMemory数据库:彻底解决测试间的数据污染问题,确保上下文查询的准确性。
内容的提问来源于stack exchange,提问作者Steve
相关产品推荐
相关产品推荐

