You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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");
    }
}

问题原因分析

  1. 手动CancellationToken提前取消:测试中手动设置5秒后取消请求,会导致请求未完成就被终止,进而触发Hosted Service的强制停止逻辑,抛出取消异常。
  2. Hosted Service与测试生命周期冲突:Web API中的Hosted Service在测试结束时可能无法正常停止,与测试的资源释放逻辑产生冲突。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 22:45:33