Skip to content

Commit 31144c5

Browse files
Add RemoveExtension and RemoveDbContext APIs for removing provider configuration (#37891)
Fixes #35126 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
1 parent 79c8ce7 commit 31144c5

7 files changed

Lines changed: 359 additions & 0 deletions

src/EFCore/DbContextOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ public virtual TExtension GetExtension<TExtension>()
9393
public abstract DbContextOptions WithExtension<TExtension>(TExtension extension)
9494
where TExtension : class, IDbContextOptionsExtension;
9595

96+
/// <summary>
97+
/// Removes the given extension from the underlying options and creates a new
98+
/// <see cref="DbContextOptions" /> with the extension removed.
99+
/// </summary>
100+
/// <typeparam name="TExtension">The type of extension to be removed.</typeparam>
101+
/// <returns>The new options instance with the extension removed.</returns>
102+
public abstract DbContextOptions WithoutExtension<TExtension>()
103+
where TExtension : class, IDbContextOptionsExtension;
104+
96105
/// <summary>
97106
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
98107
/// the same compatibility standards as public APIs. It may be changed or removed without notice in

src/EFCore/DbContextOptionsBuilder.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,17 @@ public virtual DbContextOptionsBuilder UseAsyncSeeding(Func<DbContext, bool, Can
786786
void IDbContextOptionsBuilderInfrastructure.AddOrUpdateExtension<TExtension>(TExtension extension)
787787
=> _options = _options.WithExtension(extension);
788788

789+
/// <summary>
790+
/// Removes the extension of the given type from the options. If no extension of the given type exists, this is a no-op.
791+
/// </summary>
792+
/// <remarks>
793+
/// This method is intended for use by extension methods to configure the context. It is not intended to be used in
794+
/// application code.
795+
/// </remarks>
796+
/// <typeparam name="TExtension">The type of extension to be removed.</typeparam>
797+
void IDbContextOptionsBuilderInfrastructure.RemoveExtension<TExtension>()
798+
=> _options = _options.WithoutExtension<TExtension>();
799+
789800
private DbContextOptionsBuilder WithOption(Func<CoreOptionsExtension, CoreOptionsExtension> withFunc)
790801
{
791802
((IDbContextOptionsBuilderInfrastructure)this).AddOrUpdateExtension(

src/EFCore/DbContextOptions`.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,30 @@ public override DbContextOptions WithExtension<TExtension>(TExtension extension)
5757
return new DbContextOptions<TContext>(ExtensionsMap.SetItem(type, (extension, ordinal)));
5858
}
5959

60+
/// <inheritdoc />
61+
public override DbContextOptions WithoutExtension<TExtension>()
62+
{
63+
var type = typeof(TExtension);
64+
if (!ExtensionsMap.TryGetValue(type, out var removedValue))
65+
{
66+
return this;
67+
}
68+
69+
var removedOrdinal = removedValue.Ordinal;
70+
var newMap = ExtensionsMap.Remove(type);
71+
72+
// Renormalize ordinals for extensions that followed the removed one
73+
foreach (var (key, value) in newMap)
74+
{
75+
if (value.Ordinal > removedOrdinal)
76+
{
77+
newMap = newMap.SetItem(key, (value.Extension, value.Ordinal - 1));
78+
}
79+
}
80+
81+
return new DbContextOptions<TContext>(newMap);
82+
}
83+
6084
/// <summary>
6185
/// The type of context that these options are for (<typeparamref name="TContext" />).
6286
/// </summary>

src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,6 +1163,66 @@ public static IServiceCollection ConfigureDbContext
11631163
return serviceCollection;
11641164
}
11651165

1166+
/// <summary>
1167+
/// Removes services for the given context type from the <see cref="IServiceCollection" />.
1168+
/// </summary>
1169+
/// <remarks>
1170+
/// <para>
1171+
/// This method can be used to remove the context registration in integration testing scenarios
1172+
/// where a different database provider is used for tests.
1173+
/// </para>
1174+
/// <para>
1175+
/// See <see href="https://aka.ms/efcore-docs-di">Using DbContext with dependency injection</see> for more information and examples.
1176+
/// </para>
1177+
/// </remarks>
1178+
/// <typeparam name="TContext">The type of context to be removed.</typeparam>
1179+
/// <param name="serviceCollection">The <see cref="IServiceCollection" /> to remove services from.</param>
1180+
/// <param name="removeConfigurationOnly">
1181+
/// If <see langword="true" />, only the <see cref="IDbContextOptionsConfiguration{TContext}" /> registrations will be removed;
1182+
/// the context itself will remain registered. If <see langword="false" /> (the default), all services related to the context
1183+
/// will be removed.
1184+
/// </param>
1185+
/// <returns>The same service collection so that multiple calls can be chained.</returns>
1186+
public static IServiceCollection RemoveDbContext
1187+
<[DynamicallyAccessedMembers(DbContext.DynamicallyAccessedMemberTypes)] TContext>(
1188+
this IServiceCollection serviceCollection,
1189+
bool removeConfigurationOnly = false)
1190+
where TContext : DbContext
1191+
{
1192+
Check.NotNull(serviceCollection);
1193+
1194+
if (removeConfigurationOnly)
1195+
{
1196+
var configurations = serviceCollection
1197+
.Where(d => d.ServiceType == typeof(IDbContextOptionsConfiguration<TContext>))
1198+
.ToList();
1199+
1200+
foreach (var descriptor in configurations)
1201+
{
1202+
serviceCollection.Remove(descriptor);
1203+
}
1204+
}
1205+
else
1206+
{
1207+
var descriptorsToRemove = serviceCollection
1208+
.Where(d => d.ServiceType == typeof(TContext)
1209+
|| d.ServiceType == typeof(DbContextOptions<TContext>)
1210+
|| d.ServiceType == typeof(IDbContextOptionsConfiguration<TContext>)
1211+
|| d.ServiceType == typeof(IDbContextFactorySource<TContext>)
1212+
|| d.ServiceType == typeof(IDbContextFactory<TContext>)
1213+
|| d.ServiceType == typeof(IDbContextPool<TContext>)
1214+
|| d.ServiceType == typeof(IScopedDbContextLease<TContext>))
1215+
.ToList();
1216+
1217+
foreach (var descriptor in descriptorsToRemove)
1218+
{
1219+
serviceCollection.Remove(descriptor);
1220+
}
1221+
}
1222+
1223+
return serviceCollection;
1224+
}
1225+
11661226
private static void AddCoreServices<TContextImplementation>(
11671227
IServiceCollection serviceCollection,
11681228
Action<IServiceProvider, DbContextOptionsBuilder>? optionsAction,

src/EFCore/Infrastructure/IDbContextOptionsBuilderInfrastructure.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,21 @@ public interface IDbContextOptionsBuilderInfrastructure
3636
/// <param name="extension">The extension to be added.</param>
3737
void AddOrUpdateExtension<TExtension>(TExtension extension)
3838
where TExtension : class, IDbContextOptionsExtension;
39+
40+
/// <summary>
41+
/// <para>
42+
/// Removes the extension of the given type from the options. If no extension of the given type exists, this is a no-op.
43+
/// </para>
44+
/// <para>
45+
/// This method is intended for use by extension methods to configure the context. It is not intended to be used in
46+
/// application code.
47+
/// </para>
48+
/// </summary>
49+
/// <remarks>
50+
/// See <see href="https://aka.ms/efcore-docs-providers">Implementation of database providers and extensions</see>
51+
/// for more information and examples.
52+
/// </remarks>
53+
/// <typeparam name="TExtension">The type of extension to be removed.</typeparam>
54+
void RemoveExtension<TExtension>()
55+
where TExtension : class, IDbContextOptionsExtension;
3956
}

test/EFCore.Tests/DbContextOptionsTest.cs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,76 @@ public void Can_update_an_existing_extension()
9696
Assert.Same(extension2, optionsBuilder.Options.FindExtension<FakeDbContextOptionsExtension1>());
9797
}
9898

99+
[ConditionalFact]
100+
public void Can_remove_an_existing_extension()
101+
{
102+
var optionsBuilder = new DbContextOptionsBuilder();
103+
104+
var extension1 = new FakeDbContextOptionsExtension1();
105+
var extension2 = new FakeDbContextOptionsExtension2();
106+
107+
((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension1);
108+
((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension2);
109+
110+
Assert.Equal(2, optionsBuilder.Options.Extensions.Count());
111+
112+
((IDbContextOptionsBuilderInfrastructure)optionsBuilder).RemoveExtension<FakeDbContextOptionsExtension1>();
113+
114+
Assert.Single(optionsBuilder.Options.Extensions);
115+
Assert.Null(optionsBuilder.Options.FindExtension<FakeDbContextOptionsExtension1>());
116+
Assert.Same(extension2, optionsBuilder.Options.FindExtension<FakeDbContextOptionsExtension2>());
117+
}
118+
119+
[ConditionalFact]
120+
public void Removing_non_existent_extension_is_no_op()
121+
{
122+
var optionsBuilder = new DbContextOptionsBuilder();
123+
124+
var extension = new FakeDbContextOptionsExtension1();
125+
((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);
126+
127+
Assert.Single(optionsBuilder.Options.Extensions);
128+
129+
((IDbContextOptionsBuilderInfrastructure)optionsBuilder).RemoveExtension<FakeDbContextOptionsExtension2>();
130+
131+
Assert.Single(optionsBuilder.Options.Extensions);
132+
Assert.Same(extension, optionsBuilder.Options.FindExtension<FakeDbContextOptionsExtension1>());
133+
}
134+
135+
[ConditionalFact]
136+
public void Removing_extension_from_middle_renormalizes_ordinals_and_preserves_insertion_order()
137+
{
138+
var optionsBuilder = new DbContextOptionsBuilder();
139+
140+
var extension1 = new FakeDbContextOptionsExtension1();
141+
var extension2 = new FakeDbContextOptionsExtension2();
142+
var extension3 = new FakeDbContextOptionsExtension3();
143+
144+
((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension1);
145+
((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension2);
146+
((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension3);
147+
148+
Assert.Equal(3, optionsBuilder.Options.Extensions.Count());
149+
150+
// Remove the middle extension
151+
((IDbContextOptionsBuilderInfrastructure)optionsBuilder).RemoveExtension<FakeDbContextOptionsExtension2>();
152+
153+
Assert.Equal(2, optionsBuilder.Options.Extensions.Count());
154+
var extensionsList = optionsBuilder.Options.Extensions.ToList();
155+
Assert.Same(extension1, extensionsList[0]);
156+
Assert.Same(extension3, extensionsList[1]);
157+
158+
// Add a new extension after removing the middle one - ordinals should stay contiguous
159+
var extension2New = new FakeDbContextOptionsExtension2();
160+
((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension2New);
161+
162+
Assert.Equal(3, optionsBuilder.Options.Extensions.Count());
163+
extensionsList = optionsBuilder.Options.Extensions.ToList();
164+
Assert.Same(extension1, extensionsList[0]);
165+
Assert.Same(extension3, extensionsList[1]);
166+
Assert.Same(extension2New, extensionsList[2]);
167+
}
168+
99169
[ConditionalFact]
100170
public void IsConfigured_returns_true_if_any_provider_extensions_have_been_added()
101171
{
@@ -199,6 +269,42 @@ public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
199269
}
200270
}
201271

272+
private class FakeDbContextOptionsExtension3 : IDbContextOptionsExtension
273+
{
274+
private DbContextOptionsExtensionInfo _info;
275+
276+
public DbContextOptionsExtensionInfo Info
277+
=> _info ??= new ExtensionInfo(this);
278+
279+
public bool AppliedServices { get; private set; }
280+
281+
public virtual void ApplyServices(IServiceCollection services)
282+
=> AppliedServices = true;
283+
284+
public virtual void Validate(IDbContextOptions options)
285+
{
286+
}
287+
288+
private sealed class ExtensionInfo(IDbContextOptionsExtension extension) : DbContextOptionsExtensionInfo(extension)
289+
{
290+
public override bool IsDatabaseProvider
291+
=> false;
292+
293+
public override int GetServiceProviderHashCode()
294+
=> 0;
295+
296+
public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other)
297+
=> true;
298+
299+
public override string LogFragment
300+
=> "";
301+
302+
public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
303+
{
304+
}
305+
}
306+
}
307+
202308
[ConditionalFact]
203309
public void UseModel_on_generic_builder_returns_generic_builder()
204310
{

0 commit comments

Comments
 (0)