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

ASP.NET Core 6集成测试中多数据库提供程序注册错误排查

ASP.NET Core 6 集成测试报错:同时注册多个EF数据库提供程序

报错信息

首次执行集成测试失败,抛出顶层异常:

System.InvalidOperationException: The entry point exited without ever building an IHost.

实际核心异常:

System.InvalidOperationException: Services for database providers 'Microsoft.EntityFrameworkCore.Sqlite', 'Microsoft.EntityFrameworkCore.SqlServer' have been registered in the service provider. Only a single database provider can be registered in a service provider. If possible, ensure that Entity Framework is managing its service provider by removing the call to 'UseInternalServiceProvider'. Otherwise, consider conditionally registering the database provider, or maintaining one service provider per database provider.


相关代码

Program.cs

using Serilog;

namespace TaskScheduler.Service;

public class Program
{
    private static async Task Main(string[] args)
    {
        Log.Logger = Logging.Log;

        try
        {
            var host = Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>())
                .ConfigureServices()
                .RegisterServices()
                .ConfigureLogging()
                .ConfigureSettings()
                .UseSerilog()
                .Build();

            await host.RunAsync();
        }
        catch (Exception ex)
        {
            Log.Fatal(ex, "Application terminated unexpectedly");
        }
        finally
        {
            await Log.CloseAndFlushAsync();
        }
    }
}

服务注册扩展(数据库上下文注册)

using Quartz;
using Quartz.Impl;
using Quartz.Spi;

namespace TaskScheduler.Service.Extensions;

public static class ServicesRegistrationExtension
{
    public static IHostBuilder RegisterServices(this IHostBuilder hostBuilder) =>
        hostBuilder.ConfigureServices(services => _ = services
            .AddHttpClient()
            .AddMemoryCache()
            .AddTransient<IJobDetailCreator, RestGetActivity>()
            .AddTransient<IJobDetailCreator, RestPostActivity>()
            .AddTransient(typeof(IInOutRepository<,,>), typeof(EfRepository<,,>))
            .AddTransient(typeof(ICacheRepository<,>), typeof(CacheRepository<,>))
            .AddTransient(typeof(IModelRepository<,>), typeof(ModelRepository<,>))
            .AddTransient<IAuditingRepository<ScheduleTaskModel>, ScheduleTaskAuditRepository>()
            .AddTransient<IActivityObjectRepositoryFactory, ActivityObjectRepositoryFactory>()
            .AddTransient<IActivityObjectRepository, ActivityObjectRepository<GetRestModel>>()
            .AddTransient<IActivityObjectRepository, ActivityObjectRepository<PostRestModel>>()
            .AddTransient(typeof(IActivityRepository<>), typeof(ActivityRepository<>))
            .AddDbContext<TaskSchedulerContext>(
                x => x
                    .UseSqlServer()
                    .UseQueryTrackingBehavior(
                        QueryTrackingBehavior.NoTrackingWithIdentityResolution)
                    .EnableThreadSafetyChecks()
                    .EnableDetailedErrors()
                    .EnableSensitiveDataLogging(),
                ServiceLifetime.Scoped,
                ServiceLifetime.Scoped)
            .AddDbContext<CatalogContext>(
                x => x
                    .UseSqlServer(),
                ServiceLifetime.Scoped,
                ServiceLifetime.Scoped)
            .AddTransient<IQuartzJobFactory, QuartzJobFactory>()
            .AddSingleton<ISchedulerFactory, StdSchedulerFactory>()
            .AddTransient<IJobFactory, IntegrationJobFactory>()
            .AddTransient<IServiceProvider>(s => services.BuildServiceProvider())
            .AddTransient<IDateTimeUtility, DateTimeUtility>()
            .AddQuartz(quartz => quartz.UseMicrosoftDependencyInjectionJobFactory())
            .AddQuartzHostedService(options => options.WaitForJobsToComplete = true)
            .AddHttpClient<CommonApiAuthenticationHandler>((provider, client) =>
                client.BaseAddress = new Uri(
                    provider.GetOptionsValue<SemAuthenticationOptions>().CommonApiUrl)));
}

Startup.cs

using Serilog;

namespace TaskScheduler.Service;

public class Startup
{
    public void Configure(
        IApplicationBuilder app,
        IWebHostEnvironment env,
        TaskSchedulerContext taskSchedulerContext)
    {
        if (env.IsDevelopment())
        {
            _ = app.UseDeveloperExceptionPage();
            _ = taskSchedulerContext.Database.EnsureCreated();
        }
        else
        {
            _ = app.UseHttpsRedirection();
        }

        _ = app
            .UseRouting()
            .UseCors(cors => cors
                .SetIsOriginAllowed(_ => true)
                .AllowAnyMethod()
                .AllowAnyHeader()
                .AllowCredentials())
            .UseAuthorization()
            .UseSwagger()
            .UseSwaggerUI(c => c.SwaggerEndpoint(
                $"/swagger/{Consts.SwaggerDocName}/swagger.json",
                "TaskScheduler.Service.Api.V1"))
            .UseSerilogRequestLogging()
            .UseEndpoints(endpoints => endpoints
                .MapControllers()
                .RequireAuthorization(SecurityConsts.TokenAuthorizationPolicy));
    }
}

WebApplicationFactory 派生类

using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Data.Db;
using TaskScheduler.Service.DataLayer.Sql;

namespace TaskScheduler.Service.Integration.Tests;

public class TaskServiceFactory : WebApplicationFactory<Startup>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder) =>
        builder.ConfigureServices(InjectInMemoryDb);

    private static void InjectInMemoryDb(IServiceCollection services)
    {
        _ = services.RemoveAll(typeof(DbContextOptions));
        _ = services.RemoveAll(typeof(DbContextOptions<TaskSchedulerContext>));
        _ = services.RemoveAll(typeof(TaskSchedulerContext));
        _ = services.RemoveAll(typeof(DbContextOptions<CatalogContext>));
        _ = services.RemoveAll(typeof(CatalogContext));
        _ = services.AddSingleton<DbConnection>(_ =>
        {
            var connection = new SqliteConnection("DataSource=:memory:");
            connection.Open();
            return connection;
        });
        _ = services.AddDbContext<TaskSchedulerContext>(UseSqLite)
        .AddDbContext<CatalogContext>(UseSqLite);

        using var scope = services.BuildServiceProvider().CreateScope();
        var repositoryContext = scope.ServiceProvider.GetRequiredService<TaskSchedulerContext>();
        _ = repositoryContext.Database.EnsureDeleted();
        _ = repositoryContext.Database.EnsureCreated();
    }

    private static void UseSqLite(IServiceProvider container, DbContextOptionsBuilder options)
    {
        var connection = container.GetRequiredService<DbConnection>();
        _ = options.UseSqlite(connection);
    }
}

测试代码

using FluentAssertions;
using FluentAssertions.Execution;
using System.Net;
using TaskScheduler.Service.DataLayer.Sql;

namespace TaskScheduler.Service.Integration.Tests;

public class TaskTests : IClassFixture<TaskServiceFactory>
{
    private readonly HttpClient _client;

    public TaskTests(TaskServiceFactory factory)
    {
        using var scope = factory.Services.CreateScope();
        var repositoryContext = 
            scope.ServiceProvider.GetRequiredService<TaskSchedulerContext>();
        _ = repositoryContext.Database.EnsureDeleted();
        _ = repositoryContext.Database.EnsureCreated();
        _client = factory.CreateDefaultClient();
    }

    [Fact]
    public async Task GetAllScheduleTasksTest()
    {
        var result = await _client.GetAsync("schedule-tasks");

        using var scope = new AssertionScope();
        _ = result.StatusCode.Should().Be(HttpStatusCode.OK);
    }
}

问题根源与修复方案

核心问题原因

  1. 手动注册的IServiceProvider冲突:在服务注册扩展中,AddTransient<IServiceProvider>(s => services.BuildServiceProvider())手动创建了独立的服务提供程序,EF Core会复用该程序,导致原SqlServer注册与测试中的Sqlite注册同时存在。
  2. 提前构建服务提供程序:测试工厂中提前调用services.BuildServiceProvider(),此时原SqlServer注册未被完全清理,导致EF服务程序中同时存在两种数据库驱动。

具体修复步骤

1. 移除手动注册的IServiceProvider

在ServicesRegistrationExtension的服务注册链中,删除以下代码:

.AddTransient<IServiceProvider>(s => services.BuildServiceProvider())

Quartz的UseMicrosoftDependencyInjectionJobFactory已内置服务提供程序支持,手动注册会破坏EF的服务隔离机制。

2. 调整测试工厂的数据库初始化逻辑

修改TaskServiceFactory,延迟服务构建和数据库初始化到CreateHost阶段,避免提前构建导致的冲突:

public class TaskServiceFactory : WebApplicationFactory<Startup>
{
    protected override IHost CreateHost(IHostBuilder builder)
    {
        var host = base.CreateHost(builder);
        
        // 初始化数据库
        using var scope = host.Services.CreateScope();
        var services = scope.ServiceProvider;
        
        var taskContext = services.GetRequiredService<TaskSchedulerContext>();
        taskContext.Database.EnsureDeleted();
        taskContext.Database.EnsureCreated();
        
        var catalogContext = services.GetRequiredService<CatalogContext>();
        catalogContext.Database.EnsureDeleted();
        catalogContext.Database.EnsureCreated();
        
        return host;
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder) =>
        builder.ConfigureServices(InjectInMemoryDb);

    private static void InjectInMemoryDb(IServiceCollection services)
    {
        // 移除原有上下文配置与实例
        _ = services.RemoveAll(typeof(DbContextOptions<TaskSchedulerContext>));
        _ = services.RemoveAll(typeof(TaskSchedulerContext));
        _ = services.RemoveAll(typeof(DbContextOptions<CatalogContext>));
        _ = services.RemoveAll(typeof(CatalogContext));

        // 注册Sqlite内存连接(保持打开状态)
        _ = services.AddSingleton<DbConnection>(_ =>
        {
            var connection = new SqliteConnection("DataSource=:memory:");
            connection.Open();
            return connection;
        });

        // 重新注册上下文使用Sqlite
        _ = services.AddDbContext<TaskSchedulerContext>((sp, options) =>
            options.UseSqlite(sp.GetRequiredService<DbConnection>()))
            .AddDbContext<CatalogContext>((sp, options) =>
            options.UseSqlite(sp.GetRequiredService<DbConnection>()));
    }
}

3. 简化测试类构造函数

去掉测试类中重复的数据库初始化逻辑,由工厂统一处理:

public TaskTests(TaskServiceFactory factory)
{
    _client = factory.CreateDefaultClient();
}

额外注意事项

  • 确保DbContext类的构造函数接受DbContextOptions<TContext>参数,保证DI能正确注入配置。
  • Sqlite内存数据库连接需保持打开,当前注册为Singleton并在创建时Open的方式是正确的。

内容的提问来源于stack exchange,提问作者KUTlime

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 16:37:34