Skip to content

Commit 984dab5

Browse files
committed
Make ILazyLoader not IDisposable (#32345)
Fixes #32267 The problem here is that ILazyLoader is a transient IDisposable service, which means that the service scope will keep track of instances created in the scope. However, when using context pooling, the service scope is not disposed because it is instead re-used. This means that the scope keeps getting more and more instances added, and never clears them out. The fix is to make the service not IDisposable. Instead, we create instances from our own internal factory where we keep track of the instances created. These can then be disposed and freed when the context is places back in the pool, or when the scope is disposed thus disposing the factory.
1 parent 141740f commit 984dab5

File tree

6 files changed

+119
-4
lines changed

6 files changed

+119
-4
lines changed

src/EFCore.Abstractions/Infrastructure/ILazyLoader.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ namespace Microsoft.EntityFrameworkCore.Infrastructure;
2020
/// See <see href="https://aka.ms/efcore-docs-lazy-loading">Lazy loading</see> for more information and examples.
2121
/// </para>
2222
/// </remarks>
23-
public interface ILazyLoader : IDisposable
23+
public interface ILazyLoader
2424
{
2525
/// <summary>
2626
/// Sets the given navigation as known to be completely loaded or known to be
@@ -66,4 +66,9 @@ Task LoadAsync(
6666
object entity,
6767
CancellationToken cancellationToken = default,
6868
[CallerMemberName] string navigationName = "");
69+
70+
/// <summary>
71+
/// Disposes the loader.
72+
/// </summary>
73+
void Dispose();
6974
}

src/EFCore/ChangeTracking/Internal/StateManager.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,11 @@ public virtual void Unsubscribe(bool resetting)
695695
{
696696
disposable.Dispose();
697697
}
698+
else if (resetting
699+
&& service is ILazyLoader lazyLoader)
700+
{
701+
lazyLoader.Dispose();
702+
}
698703
else if (service is not IInjectableService detachable
699704
|| detachable.Detaching(Context, entry.Entity))
700705
{

src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ public static readonly IDictionary<Type, ServiceCharacteristics> CoreServices
126126
{ typeof(IDbContextLogger), new ServiceCharacteristics(ServiceLifetime.Scoped) },
127127
{ typeof(IAdHocMapper), new ServiceCharacteristics(ServiceLifetime.Scoped) },
128128
{ typeof(ILazyLoader), new ServiceCharacteristics(ServiceLifetime.Transient) },
129+
{ typeof(ILazyLoaderFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) },
129130
{ typeof(IParameterBindingFactory), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) },
130131
{ typeof(ITypeMappingSourcePlugin), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) },
131132
{
@@ -285,12 +286,14 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices()
285286
TryAdd<IDesignTimeModel>(p => new DesignTimeModel(GetContextServices(p)));
286287
TryAdd(p => GetContextServices(p).CurrentContext);
287288
TryAdd<IDbContextOptions>(p => GetContextServices(p).ContextOptions);
289+
TryAdd<IResettableService, ILazyLoaderFactory>(p => p.GetRequiredService<ILazyLoaderFactory>());
288290
TryAdd<IResettableService, IStateManager>(p => p.GetRequiredService<IStateManager>());
289291
TryAdd<IResettableService, IDbContextTransactionManager>(p => p.GetRequiredService<IDbContextTransactionManager>());
290292
TryAdd<IEvaluatableExpressionFilter, EvaluatableExpressionFilter>();
291293
TryAdd<IValueConverterSelector, ValueConverterSelector>();
292294
TryAdd<IConstructorBindingFactory, ConstructorBindingFactory>();
293-
TryAdd<ILazyLoader, LazyLoader>();
295+
TryAdd<ILazyLoaderFactory, LazyLoaderFactory>();
296+
TryAdd<ILazyLoader>(p => p.GetRequiredService<ILazyLoaderFactory>().Create());
294297
TryAdd<IParameterBindingFactories, ParameterBindingFactories>();
295298
TryAdd<IMemberClassifier, MemberClassifier>();
296299
TryAdd<IPropertyParameterBindingFactory, PropertyParameterBindingFactory>();
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.EntityFrameworkCore.Infrastructure.Internal;
5+
6+
/// <summary>
7+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
8+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
9+
/// any release. You should only use it directly in your code with extreme caution and knowing that
10+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
11+
/// </summary>
12+
public interface ILazyLoaderFactory : IDisposable, IResettableService
13+
{
14+
/// <summary>
15+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
16+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
17+
/// any release. You should only use it directly in your code with extreme caution and knowing that
18+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
19+
/// </summary>
20+
ILazyLoader Create();
21+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.EntityFrameworkCore.Infrastructure.Internal;
5+
6+
/// <summary>
7+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
8+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
9+
/// any release. You should only use it directly in your code with extreme caution and knowing that
10+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
11+
/// </summary>
12+
public class LazyLoaderFactory : ILazyLoaderFactory
13+
{
14+
private readonly ICurrentDbContext _currentContext;
15+
private readonly IDiagnosticsLogger<DbLoggerCategory.Infrastructure> _logger;
16+
private readonly List<ILazyLoader> _loaders = new();
17+
18+
/// <summary>
19+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
20+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
21+
/// any release. You should only use it directly in your code with extreme caution and knowing that
22+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
23+
/// </summary>
24+
public LazyLoaderFactory(
25+
ICurrentDbContext currentContext,
26+
IDiagnosticsLogger<DbLoggerCategory.Infrastructure> logger)
27+
{
28+
_currentContext = currentContext;
29+
_logger = logger;
30+
}
31+
32+
/// <summary>
33+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
34+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
35+
/// any release. You should only use it directly in your code with extreme caution and knowing that
36+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
37+
/// </summary>
38+
public virtual ILazyLoader Create()
39+
{
40+
var loader = new LazyLoader(_currentContext, _logger);
41+
_loaders.Add(loader);
42+
return loader;
43+
}
44+
45+
/// <summary>
46+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
47+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
48+
/// any release. You should only use it directly in your code with extreme caution and knowing that
49+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
50+
/// </summary>
51+
public void Dispose()
52+
{
53+
foreach (var loader in _loaders)
54+
{
55+
loader.Dispose();
56+
}
57+
_loaders.Clear();
58+
}
59+
60+
/// <summary>
61+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
62+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
63+
/// any release. You should only use it directly in your code with extreme caution and knowing that
64+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
65+
/// </summary>
66+
public void ResetState()
67+
=> Dispose();
68+
69+
/// <summary>
70+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
71+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
72+
/// any release. You should only use it directly in your code with extreme caution and knowing that
73+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
74+
/// </summary>
75+
public Task ResetStateAsync(CancellationToken cancellationToken = default)
76+
{
77+
Dispose();
78+
79+
return Task.CompletedTask;
80+
}
81+
}

test/EFCore.Tests/Infrastructure/EntityFrameworkServicesBuilderTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ private static void TestMultipleScoped(Action<EntityFrameworkServicesBuilder> tr
266266
{
267267
services = context.GetService<IEnumerable<IResettableService>>().ToList();
268268

269-
Assert.Equal(3, services.Count);
269+
Assert.Equal(4, services.Count);
270270
Assert.Contains(typeof(FakeResetableService), services.Select(s => s.GetType()));
271271
Assert.Contains(typeof(StateManager), services.Select(s => s.GetType()));
272272
Assert.Contains(typeof(InMemoryTransactionManager), services.Select(s => s.GetType()));
@@ -281,7 +281,7 @@ private static void TestMultipleScoped(Action<EntityFrameworkServicesBuilder> tr
281281
{
282282
var newServices = context.GetService<IEnumerable<IResettableService>>().ToList();
283283

284-
Assert.Equal(3, newServices.Count);
284+
Assert.Equal(4, newServices.Count);
285285

286286
foreach (var service in newServices)
287287
{

0 commit comments

Comments
 (0)