X Tutup
Skip to content

Add RemoveExtension and RemoveDbContext APIs for removing provider configuration#37891

Open
Copilot wants to merge 5 commits intomainfrom
copilot/add-api-remove-provider-config
Open

Add RemoveExtension and RemoveDbContext APIs for removing provider configuration#37891
Copilot wants to merge 5 commits intomainfrom
copilot/add-api-remove-provider-config

Conversation

Copy link
Contributor

Copilot AI commented Mar 9, 2026

Since .NET 9, DbContextOptions<> uses IDbContextOptionsConfiguration<> instances for composition rather than holding configuration directly. This makes replacing a database provider in testing scenarios (e.g., swapping SQL Server for InMemory) difficult—there's no public API to remove a previously registered provider extension.

New APIs

  • IDbContextOptionsBuilderInfrastructure.RemoveExtension<TExtension>() — removes an extension from the options builder
  • DbContextOptions.WithoutExtension<TExtension>() — immutable counterpart to WithExtension, returns new options without the specified extension; returns this when the extension isn't present and renormalizes ordinals to keep them contiguous
  • IServiceCollection.RemoveDbContext<TContext>(bool removeConfiguration = false) — removes context-specific services from DI (typed options, configuration, factory, pool registrations); when removeConfiguration: true, only strips IDbContextOptionsConfiguration<TContext> registrations. Does not remove the non-generic DbContextOptions forwarding registration since it may belong to a different context.

Usage

Remove a specific provider extension via ConfigureDbContext:

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

Or fully replace a context registration in integration tests:

services.RemoveDbContext<ApplicationDbContext>();
services.AddDbContext<ApplicationDbContext>(options =>
    options.UseInMemoryDatabase("TestDb"));

Or remove only the configuration while keeping the context registered:

services.RemoveDbContext<ApplicationDbContext>(removeConfiguration: true);
services.ConfigureDbContext<ApplicationDbContext>(options =>
    options.UseInMemoryDatabase("TestDb"));
Original prompt

This section details on the original issue you should resolve

<issue_title>Add API to remove provider configuration.</issue_title>
<issue_description>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...</issue_description>

<agent_instructions>Add RemoveDbContext(bool onlyRemoveConfiguration = false)</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@cincuranet 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 futur...

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 2 commits March 9, 2026 18:49
…nfiguration

- Add RemoveExtension<TExtension>() to IDbContextOptionsBuilderInfrastructure
- Add WithoutExtension<TExtension>() to DbContextOptions and DbContextOptions<TContext>
- Implement RemoveExtension<TExtension>() in DbContextOptionsBuilder
- Add RemoveDbContext<TContext>(bool removeConfiguration) extension on IServiceCollection
- Add unit tests for all new APIs

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
…DbContext

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Copilot AI changed the title [WIP] Add API to remove provider configuration Add RemoveExtension and RemoveDbContext APIs for removing provider configuration Mar 9, 2026
@AndriySvyryd AndriySvyryd marked this pull request as ready for review March 9, 2026 19:29
@AndriySvyryd AndriySvyryd requested a review from a team as a code owner March 9, 2026 19:29
Copilot AI review requested due to automatic review settings March 9, 2026 19:29
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces new public APIs to support removing EF Core provider configuration in .NET 9+ centralized IDbContextOptionsConfiguration<TContext> scenarios, enabling provider swapping (especially in tests) without rebuilding the entire DI setup.

Changes:

  • Add IDbContextOptionsBuilderInfrastructure.RemoveExtension<TExtension>() and underlying options support via DbContextOptions.WithoutExtension<TExtension>().
  • Add IServiceCollection.RemoveDbContext<TContext>(bool removeConfiguration = false) for removing context registrations and/or centralized configuration.
  • Add unit tests covering the new behaviors.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/EFCore.Tests/DbContextTest.Services.cs Adds tests for RemoveDbContext behavior (full removal, config-only removal, re-registration).
test/EFCore.Tests/DbContextOptionsTest.cs Adds tests for removing an options extension and for no-op removal.
src/EFCore/Infrastructure/IDbContextOptionsBuilderInfrastructure.cs Adds RemoveExtension<TExtension>() to the builder infrastructure interface.
src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs Adds RemoveDbContext<TContext>(removeConfiguration: bool) DI helper.
src/EFCore/DbContextOptions.cs Adds DbContextOptions.WithoutExtension<TExtension>() API surface.
src/EFCore/DbContextOptions`.cs Implements WithoutExtension<TExtension>() for DbContextOptions<TContext>.
src/EFCore/DbContextOptionsBuilder.cs Implements RemoveExtension<TExtension>() on the builder infrastructure.

You can also share your feedback on Copilot code review. Take the survey.

@AndriySvyryd AndriySvyryd marked this pull request as draft March 9, 2026 19:53
…nal check from Equals, remove non-generic DbContextOptions in RemoveDbContext

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.


You can also share your feedback on Copilot code review. Take the survey.

…ti-context isolation test; fix XML doc

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
@AndriySvyryd AndriySvyryd marked this pull request as ready for review March 9, 2026 21:04
Copilot AI review requested due to automatic review settings March 9, 2026 21:04
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.


You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 145 to +148
protected virtual bool Equals(DbContextOptions other)
=> _extensionsMap.Count == other._extensionsMap.Count
&& _extensionsMap.Zip(other._extensionsMap)
.All(p => p.First.Value.Ordinal == p.Second.Value.Ordinal
&& p.First.Value.Extension.Info.ShouldUseSameServiceProvider(p.Second.Value.Extension.Info));
.All(p => p.First.Value.Extension.Info.ShouldUseSameServiceProvider(p.Second.Value.Extension.Info));
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DbContextOptions.Equals compares extension pairs via Zip but doesn't verify that the extension types/keys match, while GetHashCode includes the type key. If a provider's ShouldUseSameServiceProvider implementation ever returns true for a different extension type, Equals can return true while hash codes differ, breaking dictionary/set usage and service-provider caching. Consider also comparing the Key (or otherwise ensuring the two maps have identical keys in the same order) before calling ShouldUseSameServiceProvider.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +81
/// <inheritdoc />
public override DbContextOptions WithoutExtension<TExtension>()
{
var type = typeof(TExtension);
if (!ExtensionsMap.TryGetValue(type, out var removedValue))
{
return this;
}

var removedOrdinal = removedValue.Ordinal;
var newMap = ExtensionsMap.Remove(type);

// Renormalize ordinals for extensions that followed the removed one
foreach (var (key, value) in newMap)
{
if (value.Ordinal > removedOrdinal)
{
newMap = newMap.SetItem(key, (value.Extension, value.Ordinal - 1));
}
}

return new DbContextOptions<TContext>(newMap);
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WithoutExtension includes ordinal renormalization logic, but the tests only cover removing an extension and removing a non-existent extension. Consider adding a regression test that removes an extension from the middle and then adds a new extension, asserting that Extensions enumeration preserves insertion order (i.e., ordinals stay contiguous and no duplicates are produced). This would directly exercise the renormalization branch.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +3934 to +3955
[ConditionalFact]
public void RemoveDbContext_does_not_remove_services_for_different_context()
{
var serviceCollection = new ServiceCollection()
.AddDbContext<ConstructorTestContext1A>(b => b.EnableServiceProviderCaching(false)
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.ConfigureWarnings(w => w.Default(WarningBehavior.Throw)))
.AddDbContext<ConstructorTestContextWithOC3A>(b => b.EnableServiceProviderCaching(false)
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.ConfigureWarnings(w => w.Default(WarningBehavior.Throw)));

serviceCollection.RemoveDbContext<ConstructorTestContext1A>();

Assert.DoesNotContain(serviceCollection, d => d.ServiceType == typeof(ConstructorTestContext1A));
Assert.DoesNotContain(serviceCollection, d => d.ServiceType == typeof(DbContextOptions<ConstructorTestContext1A>));
Assert.DoesNotContain(serviceCollection, d => d.ServiceType == typeof(IDbContextOptionsConfiguration<ConstructorTestContext1A>));

Assert.Contains(serviceCollection, d => d.ServiceType == typeof(ConstructorTestContextWithOC3A));
Assert.Contains(serviceCollection, d => d.ServiceType == typeof(DbContextOptions<ConstructorTestContextWithOC3A>));
Assert.Contains(serviceCollection, d => d.ServiceType == typeof(DbContextOptions));
Assert.Contains(serviceCollection, d => d.ServiceType == typeof(IDbContextOptionsConfiguration<ConstructorTestContextWithOC3A>));
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current RemoveDbContext tests don't cover the AddDbContext<TContextService,TContextImplementation> case where the service type differs from the implementation type (e.g., registering a base DbContext type and a derived implementation). Adding a test for that scenario would prevent regressions where RemoveDbContext leaves the TContextService descriptor behind, causing resolution failures after removal.

Copilot generated this review using guidance from repository custom instructions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add API to remove provider configuration.

4 participants

X Tutup