Search code examples
c#asp.net-coreentity-framework-coreintegration-testingasp.net-core-6.0

Multiple DB providers error in integration tests in ASP.NET Core 6 app


I have a relatively simple ASP.NET Core 6 application that I want to cover by integration tests.

I followed several tutorials, but my first test fails on

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

The real problem is

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

I can't figure out what is wrong. I tried several approaches, none of them work.

Does anybody have an idea what I'm doing wrong?


Program

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();
        }
    }
}

Registering DB context in the application

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

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 descendant

using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Data.Common;
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);
    }
}

Tests

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);
    }
}


Solution

  • The error indicates you've create a dbcontext with two database providers.

    Since you've replaced DbContextOptions in IServiceCollection with these lines:

    _ = services.RemoveAll(typeof(DbContextOptions<TaskSchedulerContext>));
    
    
    
    _ = services.AddSingleton<DbConnection>(_ =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();
                return connection;
            });
    
     _ = services.AddDbContext<TaskSchedulerContext>(UseSqLite)
            .AddDbContext<CatalogContext>(UseSqLite);
    
    private static void UseSqLite(IServiceProvider container, DbContextOptionsBuilder options)
        {
            var connection = container.GetRequiredService<DbConnection>();
            _ = options.UseSqlite(connection);
        }
    

    Check your dbcontext if you've configured other database providers(Microsoft.EntityFrameworkCore.SqlServer),I reproduced the error with the following steps:

    services.AddDbContext<SomeContext>(op=>op.UseSqlServer(connectstring))
    

    in dbcontext:

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)   
    {        
               
        optionsBuilder.UseSqlServer(connectstring);
    }
    

    it works well till now,but after I tried :

    services.AddDbContext<SomeContext>(op=>op.UseSqlServer(connectstring))
    
    builder.Services.RemoveAll(typeof(DbContextOptions<SomeContext>))
    
    services.AddDbContext<SomeContext>(op=>op.InMemoryDatabase(dbname))
    

    I got the same error

    and fixed it with remove

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)   
        {        
                   
            optionsBuilder.UseSqlServer(connectstring);
        }
    

    or add a judgement:

    if(!optionsBuilder.IsConfigured) 
    

    if you still get the error please check if you congiured the database provider any where else and provide the minimal codes that could reproduce the error