Add RemoveExtension and RemoveDbContext APIs for removing provider configuration#37891
Add RemoveExtension and RemoveDbContext APIs for removing provider configuration#37891
Conversation
…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>
There was a problem hiding this comment.
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 viaDbContextOptions.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.
…nal check from Equals, remove non-generic DbContextOptions in RemoveDbContext Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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.
| 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)); |
There was a problem hiding this comment.
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.
| /// <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); |
There was a problem hiding this comment.
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.
| [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>)); | ||
| } |
There was a problem hiding this comment.
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.
Since .NET 9,
DbContextOptions<>usesIDbContextOptionsConfiguration<>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 builderDbContextOptions.WithoutExtension<TExtension>()— immutable counterpart toWithExtension, returns new options without the specified extension; returnsthiswhen the extension isn't present and renormalizes ordinals to keep them contiguousIServiceCollection.RemoveDbContext<TContext>(bool removeConfiguration = false)— removes context-specific services from DI (typed options, configuration, factory, pool registrations); whenremoveConfiguration: true, only stripsIDbContextOptionsConfiguration<TContext>registrations. Does not remove the non-genericDbContextOptionsforwarding registration since it may belong to a different context.Usage
Remove a specific provider extension via
ConfigureDbContext:Or fully replace a context registration in integration tests:
Or remove only the configuration while keeping the context registered:
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:The exact error:
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):
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.