efcore icon indicating copy to clipboard operation
efcore copied to clipboard

Add API to remove provider configuration.

Open Tealons opened this issue 1 year ago • 37 comments

After upgrading to .NET 9 I get the following error on db.Database.EnsureCreated(); in the following code:

using (var scope = sp.CreateScope())
{
    var scopedServices = scope.ServiceProvider;
    var db = scopedServices.GetRequiredService<ApplicationDbContext>();
    var logger = scopedServices
        .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();

    // Ensure the database is created.
    db.Database.EnsureCreated();

    try
    {
        // Seed the database with test data.
        // Utilities.InitializeDbForTests(db);
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "An error occurred seeding the " +
            "database with test messages. Error: {Message}", ex.Message);
    }
}

Image

The exact error:

System.InvalidOperationException
  HResult=0x80131509
  Message=Services for database providers 'Microsoft.EntityFrameworkCore.SqlServer', 'Microsoft.EntityFrameworkCore.InMemory' 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.
  Source=Microsoft.EntityFrameworkCore
  StackTrace:
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.Initialize(IServiceProvider scopedProvider, DbContextOptions contextOptions, DbContext context)
   at Microsoft.EntityFrameworkCore.DbContext.get_ContextServices()
   at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()
   at Microsoft.EntityFrameworkCore.DbContext.Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure<System.IServiceProvider>.get_Instance()
   at Microsoft.EntityFrameworkCore.Infrastructure.Internal.InfrastructureExtensions.GetService(IInfrastructure`1 accessor, Type serviceType)
   at Microsoft.EntityFrameworkCore.Infrastructure.Internal.InfrastructureExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.get_Dependencies()
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.EnsureCreated()
   at SpiderIoT.Test.CustomWebApplicationFactory`1.<>c.<ConfigureWebHost>b__0_0(IServiceCollection services) in C:\Source\SpiderIoT\SpiderIoT.Test\CustomWebApplicationFactory.cs:line 54
   at Microsoft.Extensions.Hosting.HostBuilder.InitializeServiceProvider()
   at Microsoft.Extensions.Hosting.HostBuilder.Build()
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateHost(IHostBuilder builder)
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.ConfigureHostBuilder(IHostBuilder hostBuilder)
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.EnsureServer()
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.get_Services()
   at SpiderIoT.Test.APITest..ctor(CustomWebApplicationFactory`1 factory) in C:\Source\SpiderIoT\SpiderIoT.Test\APITest.cs:line 36
   at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)

My feeling is that removing the previous ApplicationDbContext is not working properly? This is my code (which was adjusted according to the .NET 9 docs, but is still not working):

 // Remove the app's ApplicationDbContext registration.
 var dbContextDescriptor = services.SingleOrDefault(
   d => d.ServiceType ==
   typeof(DbContextOptions<ApplicationDbContext>));

 services.Remove(dbContextDescriptor);

 var dbConnectionDescriptor = services.SingleOrDefault(
     d => d.ServiceType ==
         typeof(DbConnection));

 services.Remove(dbConnectionDescriptor);

 // Add ApplicationDbContext using an in-memory database for testing.
 services.AddDbContext<ApplicationDbContext>(options =>
 {
     options.UseLazyLoadingProxies();
     options.UseInMemoryDatabase("InMemoryDbForTesting");
 });

After hours of Googling and trying stuff out I'm a bit lost how this should work in .NET 9...

Tealons avatar Nov 16 '24 16:11 Tealons

Not sure how or why, but removing the IDbContextOptionsConfiguration<ApplicationDbContext> did the trick:


 var dbContextDescriptor = services.SingleOrDefault(
   d => d.ServiceType ==
   typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

Tealons avatar Nov 16 '24 19:11 Tealons

@cincuranet: closing this feels a bit wrong. The documentation like: https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-9.0#customize-webapplicationfactory will give the error I described? And is my solution the right way to do this? Or is there a better way?

Tealons avatar Nov 18 '24 21:11 Tealons

I'm experiencing this same exact issue. In my case, I'm using .NET 8 but have some EF-related packages -- such as EF core -- that I've updated to v9.0.0. Can this be re-opened, @cincuranet? If not, @Tealons perhaps we should create a new thread for this issue?

I would also like to understand if there's been an intentional change in behavior.

  • If yes, shouldn't the documentation @Tealons linked above be updated?
  • If no, I think we need a bug tracking this.

kymarti avatar Nov 19 '24 03:11 kymarti

You don't want to mix multiple providers with single DbContext. Manually removing IDbContextOptionsConfiguration works, but you're starting to touch internal infrastructure of EF and the underlying implementation can change in the future.

I guess you're doing testing. It would be way better to have setup for tests that registers (only) your desired provider and setup for production. Using methods like IHostingEnvironment.IsProduction, etc. or #ifdef or similar.

cincuranet avatar Nov 19 '24 09:11 cincuranet

@cincuranet I'm concerned about this one, since there is an indication that this is a regression.

@Tealons @kymarti Can one of you please attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing so that we can investigate.

ajcvickers avatar Nov 19 '24 10:11 ajcvickers

Sure, let me try to put something together @ajcvickers , but if you have time @Tealons feel free to go ahead and put something together as well.

kymarti avatar Nov 19 '24 17:11 kymarti

@ajcvickers I managed to reproduce the issue in this project EfCore .net9 Test

If you downgrade all packages to .net 8 the tests will succeed, but with .net 9 this exception occurs.

System.ArgumentException : Could not initialize database ---- System.InvalidOperationException : Services for database providers 'Microsoft.EntityFrameworkCore.SqlServer', 'Microsoft.EntityFrameworkCore.InMemory' 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.

loekensgard avatar Nov 20 '24 13:11 loekensgard

Amazing, thanks @loekensgard ! I didn't get a chance to put a sample project together yesterday.

kymarti avatar Nov 20 '24 16:11 kymarti

Just run into this trying to update from 8 to 9: 8 works, 9 doesn't. So I guess we're staying with 8.

rwb196884 avatar Nov 22 '24 15:11 rwb196884

Note for team: I am able to reproduce this and I am investigating. It looks like a regression in ASP.NET testing.

ajcvickers avatar Nov 26 '24 11:11 ajcvickers

Looks like this is a change in the behavior WebApplicationFactory when used in tests. Two projects are needed to repro this: a simple web app with this code:

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        builder.Services.AddDbContext<ContextOfDoom>(options => options.UseSqlServer());
        builder.Build().Run();
    }
}


public class ContextOfDoom : DbContext
{
    public ContextOfDoom(DbContextOptions<ContextOfDoom> options) : base(options) { }
}

And then a test project with a test and a WebApplicationFactory:

public class TestWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            services.AddDbContext<ContextOfDoom>((_, context) => context.UseInMemoryDatabase("DB"));
            var serviceProvider = services.BuildServiceProvider();
            using var scope = serviceProvider.CreateScope();
            var context = serviceProvider.GetService<ContextOfDoom>()!;
            _ = context.Model;
        });
    }
}

public class SomeTests : IClassFixture<TestWebApplicationFactory>
{
    public SomeTests(TestWebApplicationFactory factory)
    {
        factory.WithWebHostBuilder(_ => { }).CreateClient();
    }

    [Fact]
    public async Task Get_Test()
    {
    }
}

When running the tests in EF8, the web host resolver is not used, and so that Main method is not run, and only the in-memory database is registered.

When running the tests in EF9, the web host resolver runs and so both the SQL Server and in-memory databases are registered, resulting in:

System.InvalidOperationException
Services for database providers 'Microsoft.EntityFrameworkCore.SqlServer', 'Microsoft.EntityFrameworkCore.InMemory' 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.
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.Initialize(IServiceProvider scopedProvider, DbContextOptions contextOptions, DbContext context)
   at Microsoft.EntityFrameworkCore.DbContext.get_ContextServices()
   at Microsoft.EntityFrameworkCore.DbContext.get_Model()
   at TestWebApplicationFactory.<>c.<ConfigureWebHost>b__0_0(IServiceCollection services) in D:\code\repros\WebApplication1\WebApplication1\WebApplication1.Tests\SomeTests.cs:line 18
   at Microsoft.Extensions.Hosting.HostApplicationBuilder.HostBuilderAdapter.ApplyChanges()
   at Microsoft.Extensions.Hosting.HostApplicationBuilder.Build()
   at Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build()
   at Program.Main(String[] args) in D:\code\repros\WebApplication1\WebApplication1\WebApplication1\Program.cs:line 9
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)
--- End of stack trace from previous location ---
   at Microsoft.Extensions.Hosting.HostFactoryResolver.HostingListener.CreateHost()
   at Microsoft.Extensions.Hosting.HostFactoryResolver.<>c__DisplayClass10_0.<ResolveHostFactory>b__0(String[] args)
   at Microsoft.AspNetCore.Mvc.Testing.DeferredHostBuilder.Build()
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateHost(IHostBuilder builder)
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.DelegatedWebApplicationFactory.CreateHost(IHostBuilder builder)
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.ConfigureHostBuilder(IHostBuilder hostBuilder)
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.EnsureServer()
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateDefaultClient(DelegatingHandler[] handlers)
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateDefaultClient(Uri baseAddress, DelegatingHandler[] handlers)
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateClient(WebApplicationFactoryClientOptions options)
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateClient()
   at SomeTests..ctor(TestWebApplicationFactory factory) in D:\code\repros\WebApplication1\WebApplication1\WebApplication1.Tests\SomeTests.cs:line 27
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)

ajcvickers avatar Nov 26 '24 12:11 ajcvickers

@AndriySvyryd Assigning to you since you did some work in this area in EF8/9. Root cause could be an external breaking change in the WebApplicationFactory, or it could be something we are doing different.

ajcvickers avatar Nov 26 '24 12:11 ajcvickers

I usually remove the current registered service:

var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<WfEksternDbContext>));

if (descriptor != null)
{
    services.Remove(descriptor);
}

ErikEJ avatar Nov 26 '24 13:11 ErikEJ

@ErikEJ, it appears that this issue persists even after removing the currently registered service. In my test project, I have the same configuration, but the error still occurs when running .NET 9.

However @Tealons found a workaround, but this could potentially lead to unexpected behavior?

Not sure how or why, but removing the IDbContextOptionsConfiguration<ApplicationDbContext> did the trick:


 var dbContextDescriptor = services.SingleOrDefault(
   d => d.ServiceType ==
   typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

loekensgard avatar Nov 26 '24 13:11 loekensgard

@ErikEJ Yeah, I saw that in one of the repros--doesn't help in 9.

ajcvickers avatar Nov 26 '24 13:11 ajcvickers

Seeing the same issue, tagging myself in for updates.

jaliyaudagedara avatar Dec 02 '24 22:12 jaliyaudagedara

Seeing same issue, tagging myself as well.

VictorioBerra avatar Dec 05 '24 16:12 VictorioBerra

oh hai. Yep, updating my Pluralsight course demos to .NET 9 and ran into this. Will keep eyes on it! Thanks for everyone's contributions to the issue. (Update: my custom web factory has always removed both typeof(DbContextOptions<PubContext>) and typeof(PubContext) )

julielerman avatar Dec 10 '24 22:12 julielerman

I'm seeing this as well - just pitching in. Removing the IDbContextOptionsConfiguration<ApplicationDbContext> did remove the error, but as mentioned it's probably not the way to fix it.

jonas-lomholdt avatar Dec 16 '24 14:12 jonas-lomholdt

I was comparing debugging the .net 8 versin and .net 9. The effect of removing the dbcontext and dbcontextoptions on the services is the same. However, when adding in my two sqlite services, there is a difference. Here is the code for adding in SQLite:

// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
    var connection = new SqliteConnection("DataSource=:memory:");
    connection.Open();
    return connection;
});

services.AddDbContext<PubContext>((container, options) =>
{
    var connection = container.GetRequiredService<DbConnection>();
    options.UseSqlite(connection);
});

What's extra in EF9 is the IDbContextOptionsConfiguration (first highlighted row): Image Image

Also I noticed that after my code creawtes the customwebapplicationfactory which does the above re-org and then calls GetRequiredSErvice, it's hitting the adddbcontext in program again...the one I just removed.

using (var scope = appFactory.Services.CreateScope())
{
    var scopedServices = scope.ServiceProvider;
    var db = scopedServices.GetRequiredService<PubContext>();
    db.Database.EnsureCreated();
 }

and the failure happens when calling EnsureCreated.

That appFactory object is the instance of the customwebApplicationFactory and it's CreateScope method is where I'm removing the original services (for sqlserver) and adding in the new one (for SqlIte)

Not sure if this is useful but there it is....

julielerman avatar Dec 22 '24 22:12 julielerman

I ran into the same issue after upgrading from EF8 to 9. My Startup. cs has four DI DBContexts, so I made sure to remove all four, even though my tests only use three of them. I also changed the lines in my CustomWebApplicationFactory from:

    ServiceDescriptor descriptor1 = services.SingleOrDefault(
                    d => d.ServiceType == typeof(DbContextOptions<RVSAccountContext>))!;

to

     ServiceDescriptor descriptor1 = services.SingleOrDefault(
                    d => d.ServiceType == typeof(IDbContextOptionsConfiguration<RVSAccountContext>))!;

My tests are running again.

JYasgarYTGI avatar Dec 30 '24 03:12 JYasgarYTGI

Unfortunately the Microsoft staff said this different error message would be tracked here: https://github.com/dotnet/efcore/issues/35179 And closed that issue.

ericwood8 avatar Dec 30 '24 17:12 ericwood8

Unfortunately the Microsoft staff said this different error message would be tracked here: #35179 And closed that issue.

I think you've got mixed up: on thread #35179 @ajcvickers says to come to this thread.

rwb196884 avatar Dec 30 '24 18:12 rwb196884

@rwb196884 Yes. That is what I was trying to communicate.

ericwood8 avatar Dec 30 '24 18:12 ericwood8

Yes, @JYasgarYTGI nailed it (my tests pass now, too). But I only have a few basic tests and am wondering if we need to be concerned about side effects of using the IDbContextOptionsConfiguration interface over the DbContextOptions implementation. And it aligns with my discovery that IDbCOntextOptionsConfiguration is "left over" otherwise (as per my testing above). Thanks Jack!

julielerman avatar Dec 30 '24 20:12 julielerman

I have the same problem, for now I will have to stay on .NET8. I tag myself too.

pampua84 avatar Jan 07 '25 09:01 pampua84

Sorry that it took so long for the response. This is by design.

Starting with 9 DbContextOptions<> in the service provider doesn't contain any configuration itself, instead it uses all the registered IDbContextOptionsConfiguration<> instances to build the configuration. This was done to enable options composition, so that a context can be configured without registering it, this is useful for Aspire as it applies some default configuration.

The best workaround currently is to remove IDbContextOptionsConfiguration<> as shown above. I'll add this to breaking changes.

Since there isn't an API to remove a provider configuration, we can use this issue to track adding it. This is a fairly advanced scenario, so it doesn't need to be easily discoverable. I propose the following:

services.ConfigureDbContext<TestContext>(options =>
    ((IDbContextOptionsBuilderInfrastructure)options).RemoveExtension<SqlServerOptionsExtension>());

AndriySvyryd avatar Jan 07 '25 22:01 AndriySvyryd

@AndriySvyryd if it is by design, surely this should be edited in the documentation of learn.microsoft .NET 9 ASAP?

Anyone currently following the documentation, will hit a very unpleasant surprise "I did exactly as the docs said... WHY DOESN'T IT WORK?"

BlNARYFOUR avatar Jan 28 '25 15:01 BlNARYFOUR

@BlNARYFOUR Thanks for pointing this out. Filed https://github.com/dotnet/AspNetCore.Docs/issues/34584

Once the specialized API is added https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests#customize-webapplicationfactory should be updated

AndriySvyryd avatar Jan 28 '25 20:01 AndriySvyryd

I could not figure out how to solve it from the comments, but this blog post helped med out. In case anyone stumples upon this thread and is as confused as I was.

https://www.devgem.io/posts/resolving-multiple-ef-core-database-providers-in-asp-net-core-integration-testing

dadaPipes avatar Feb 07 '25 15:02 dadaPipes