Skip to content

Commit a149ce7

Browse files
authored
Fixed lazy loading thread safety (dotnet#35529)
Fixes dotnet#35528
1 parent 82e9c23 commit a149ce7

File tree

2 files changed

+224
-38
lines changed

2 files changed

+224
-38
lines changed

src/EFCore/Infrastructure/Internal/LazyLoader.cs

Lines changed: 93 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System.Collections.Concurrent;
55
using System.Diagnostics.CodeAnalysis;
66
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
8+
using Microsoft.EntityFrameworkCore.ChangeTracking;
79
using Microsoft.EntityFrameworkCore.Internal;
810

911
namespace Microsoft.EntityFrameworkCore.Infrastructure.Internal;
@@ -20,7 +22,8 @@ public class LazyLoader : ILazyLoader, IInjectableService
2022
private bool _disposed;
2123
private bool _detached;
2224
private IDictionary<string, bool>? _loadedStates;
23-
private readonly ConcurrentDictionary<(object Entity, string NavigationName), bool> _isLoading = new(NavEntryEqualityComparer.Instance);
25+
private readonly Lock _isLoadingLock = new Lock();
26+
private readonly Dictionary<(object Entity, string NavigationName), (TaskCompletionSource TaskCompletionSource, AsyncLocal<int> Depth)> _isLoading = new(NavEntryEqualityComparer.Instance);
2427
private HashSet<string>? _nonLazyNavigations;
2528

2629
/// <summary>
@@ -107,30 +110,56 @@ public virtual void Load(object entity, [CallerMemberName] string navigationName
107110
Check.NotEmpty(navigationName, nameof(navigationName));
108111

109112
var navEntry = (entity, navigationName);
110-
if (_isLoading.TryAdd(navEntry, true))
113+
114+
bool exists;
115+
(TaskCompletionSource TaskCompletionSource, AsyncLocal<int> Depth) isLoadingValue;
116+
117+
lock (_isLoadingLock)
118+
{
119+
ref var refIsLoadingValue = ref CollectionsMarshal.GetValueRefOrAddDefault(_isLoading, navEntry, out exists);
120+
if (!exists)
121+
{
122+
refIsLoadingValue = (new(), new());
123+
}
124+
isLoadingValue = refIsLoadingValue!;
125+
isLoadingValue.Depth.Value++;
126+
}
127+
128+
if (exists)
129+
{
130+
// Only waits for the outermost call on the call stack. See #35528.
131+
if (isLoadingValue.Depth.Value == 1)
132+
{
133+
isLoadingValue.TaskCompletionSource.Task.Wait();
134+
}
135+
return;
136+
}
137+
138+
try
111139
{
112-
try
140+
// ShouldLoad is called after _isLoading.Add because it could attempt to load the property. See #13138.
141+
if (ShouldLoad(entity, navigationName, out var entry))
113142
{
114-
// ShouldLoad is called after _isLoading.Add because it could attempt to load the property. See #13138.
115-
if (ShouldLoad(entity, navigationName, out var entry))
143+
try
116144
{
117-
try
118-
{
119-
entry.Load(
120-
_queryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution
121-
? LoadOptions.ForceIdentityResolution
122-
: LoadOptions.None);
123-
}
124-
catch
125-
{
126-
entry.IsLoaded = false;
127-
throw;
128-
}
145+
entry.Load(
146+
_queryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution
147+
? LoadOptions.ForceIdentityResolution
148+
: LoadOptions.None);
149+
}
150+
catch
151+
{
152+
entry.IsLoaded = false;
153+
throw;
129154
}
130155
}
131-
finally
156+
}
157+
finally
158+
{
159+
isLoadingValue.TaskCompletionSource.TrySetResult();
160+
lock (_isLoadingLock)
132161
{
133-
_isLoading.TryRemove(navEntry, out _);
162+
_isLoading.Remove(navEntry);
134163
}
135164
}
136165
}
@@ -150,31 +179,57 @@ public virtual async Task LoadAsync(
150179
Check.NotEmpty(navigationName, nameof(navigationName));
151180

152181
var navEntry = (entity, navigationName);
153-
if (_isLoading.TryAdd(navEntry, true))
182+
183+
bool exists;
184+
(TaskCompletionSource TaskCompletionSource, AsyncLocal<int> Depth) isLoadingValue;
185+
186+
lock (_isLoadingLock)
187+
{
188+
ref var refIsLoadingValue = ref CollectionsMarshal.GetValueRefOrAddDefault(_isLoading, navEntry, out exists);
189+
if (!exists)
190+
{
191+
refIsLoadingValue = (new(), new());
192+
}
193+
isLoadingValue = refIsLoadingValue!;
194+
isLoadingValue.Depth.Value++;
195+
}
196+
197+
if (exists)
198+
{
199+
// Only waits for the outermost call on the call stack. See #35528.
200+
if (isLoadingValue.Depth.Value == 1)
201+
{
202+
await isLoadingValue.TaskCompletionSource.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
203+
}
204+
return;
205+
}
206+
207+
try
154208
{
155-
try
209+
// ShouldLoad is called after _isLoading.Add because it could attempt to load the property. See #13138.
210+
if (ShouldLoad(entity, navigationName, out var entry))
156211
{
157-
// ShouldLoad is called after _isLoading.Add because it could attempt to load the property. See #13138.
158-
if (ShouldLoad(entity, navigationName, out var entry))
212+
try
159213
{
160-
try
161-
{
162-
await entry.LoadAsync(
163-
_queryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution
164-
? LoadOptions.ForceIdentityResolution
165-
: LoadOptions.None,
166-
cancellationToken).ConfigureAwait(false);
167-
}
168-
catch
169-
{
170-
entry.IsLoaded = false;
171-
throw;
172-
}
214+
await entry.LoadAsync(
215+
_queryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution
216+
? LoadOptions.ForceIdentityResolution
217+
: LoadOptions.None,
218+
cancellationToken).ConfigureAwait(false);
219+
}
220+
catch
221+
{
222+
entry.IsLoaded = false;
223+
throw;
173224
}
174225
}
175-
finally
226+
}
227+
finally
228+
{
229+
isLoadingValue.TaskCompletionSource.TrySetResult();
230+
lock (_isLoadingLock)
176231
{
177-
_isLoading.TryRemove(navEntry, out _);
232+
_isLoading.Remove(navEntry);
178233
}
179234
}
180235
}

test/EFCore.Specification.Tests/LoadTestBase.cs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5035,6 +5035,60 @@ public virtual void Setting_navigation_to_null_is_detected_by_local_DetectChange
50355035
Assert.Equal(EntityState.Deleted, childEntry.State);
50365036
}
50375037

5038+
[ConditionalTheory] // Issue #35528
5039+
[InlineData(false, false)]
5040+
[InlineData(true, false)]
5041+
[InlineData(false, true)]
5042+
[InlineData(true, true)]
5043+
public virtual async Task Lazy_loading_is_thread_safe(bool noTracking, bool async)
5044+
{
5045+
using var context = CreateContext(lazyLoadingEnabled: true);
5046+
5047+
//Creating another context to avoid caches
5048+
using var context2 = CreateContext(lazyLoadingEnabled: true);
5049+
5050+
IQueryable<Parent> query = context.Set<Parent>();
5051+
IQueryable<Parent> query2 = context2.Set<Parent>();
5052+
5053+
if (noTracking)
5054+
{
5055+
query = query.AsNoTracking();
5056+
query2 = query2.AsNoTracking();
5057+
}
5058+
5059+
var parent = query.Single();
5060+
5061+
var children = (await parent.LazyLoadChildren(async))?.Select(x => x.Id).OrderBy(x => x).ToList();
5062+
var singlePkToPk = (await parent.LazyLoadSinglePkToPk(async))?.Id;
5063+
var single = (await parent.LazyLoadSingle(async))?.Id;
5064+
var childrenAk = (await parent.LazyLoadChildrenAk(async))?.Select(x => x.Id).OrderBy(x => x).ToList();
5065+
var singleAk = (await parent.LazyLoadSingleAk(async))?.Id;
5066+
var childrenShadowFk = (await parent.LazyLoadChildrenShadowFk(async))?.Select(x => x.Id).OrderBy(x => x).ToList();
5067+
var singleShadowFk = (await parent.LazyLoadSingleShadowFk(async))?.Id;
5068+
var childrenCompositeKey = (await parent.LazyLoadChildrenCompositeKey(async))?.Select(x => x.Id).OrderBy(x => x).ToList();
5069+
var singleCompositeKey = (await parent.LazyLoadSingleCompositeKey(async))?.Id;
5070+
5071+
var parent2 = query2.Single();
5072+
5073+
var parallelOptions = new ParallelOptions
5074+
{
5075+
MaxDegreeOfParallelism = Environment.ProcessorCount * 500
5076+
};
5077+
5078+
await Parallel.ForAsync(0, 50000, parallelOptions, async (i, ct) =>
5079+
{
5080+
Assert.Equal(children, (await parent2.LazyLoadChildren(async))?.Select(x => x.Id).OrderBy(x => x).ToList());
5081+
Assert.Equal(singlePkToPk, (await parent2.LazyLoadSinglePkToPk(async))?.Id);
5082+
Assert.Equal(single, (await parent2.LazyLoadSingle(async))?.Id);
5083+
Assert.Equal(childrenAk, (await parent2.LazyLoadChildrenAk(async))?.Select(x => x.Id).OrderBy(x => x).ToList());
5084+
Assert.Equal(singleAk, (await parent2.LazyLoadSingleAk(async))?.Id);
5085+
Assert.Equal(childrenShadowFk, (await parent2.LazyLoadChildrenShadowFk(async))?.Select(x => x.Id).OrderBy(x => x).ToList());
5086+
Assert.Equal(singleShadowFk, (await parent2.LazyLoadSingleShadowFk(async))?.Id);
5087+
Assert.Equal(childrenCompositeKey, (await parent2.LazyLoadChildrenCompositeKey(async))?.Select(x => x.Id).OrderBy(x => x).ToList());
5088+
Assert.Equal(singleCompositeKey, (await parent2.LazyLoadSingleCompositeKey(async))?.Id);
5089+
});
5090+
}
5091+
50385092
private static void SetState(
50395093
DbContext context,
50405094
object entity,
@@ -5092,6 +5146,17 @@ public SinglePkToPk SinglePkToPk
50925146
set => _singlePkToPk = value;
50935147
}
50945148

5149+
public async Task<SinglePkToPk> LazyLoadSinglePkToPk(bool async)
5150+
{
5151+
if (async)
5152+
{
5153+
await Loader.LoadAsync(this, default, nameof(SinglePkToPk));
5154+
return _singlePkToPk;
5155+
}
5156+
5157+
return SinglePkToPk;
5158+
}
5159+
50955160
public Single Single
50965161
{
50975162
get => Loader.Load(this, ref _single);
@@ -5121,35 +5186,101 @@ public IEnumerable<ChildAk> ChildrenAk
51215186
set => _childrenAk = value;
51225187
}
51235188

5189+
public async Task<IEnumerable<ChildAk>> LazyLoadChildrenAk(bool async)
5190+
{
5191+
if (async)
5192+
{
5193+
await Loader.LoadAsync(this, default, nameof(ChildrenAk));
5194+
return _childrenAk;
5195+
}
5196+
5197+
return ChildrenAk;
5198+
}
5199+
51245200
public SingleAk SingleAk
51255201
{
51265202
get => Loader.Load(this, ref _singleAk);
51275203
set => _singleAk = value;
51285204
}
51295205

5206+
public async Task<SingleAk> LazyLoadSingleAk(bool async)
5207+
{
5208+
if (async)
5209+
{
5210+
await Loader.LoadAsync(this, default, nameof(SingleAk));
5211+
return _singleAk;
5212+
}
5213+
5214+
return SingleAk;
5215+
}
5216+
51305217
public IEnumerable<ChildShadowFk> ChildrenShadowFk
51315218
{
51325219
get => Loader.Load(this, ref _childrenShadowFk);
51335220
set => _childrenShadowFk = value;
51345221
}
51355222

5223+
public async Task<IEnumerable<ChildShadowFk>> LazyLoadChildrenShadowFk(bool async)
5224+
{
5225+
if (async)
5226+
{
5227+
await Loader.LoadAsync(this, default, nameof(ChildrenShadowFk));
5228+
return _childrenShadowFk;
5229+
}
5230+
5231+
return ChildrenShadowFk;
5232+
}
5233+
51365234
public SingleShadowFk SingleShadowFk
51375235
{
51385236
get => Loader.Load(this, ref _singleShadowFk);
51395237
set => _singleShadowFk = value;
51405238
}
51415239

5240+
public async Task<SingleShadowFk> LazyLoadSingleShadowFk(bool async)
5241+
{
5242+
if (async)
5243+
{
5244+
await Loader.LoadAsync(this, default, nameof(SingleShadowFk));
5245+
return _singleShadowFk;
5246+
}
5247+
5248+
return SingleShadowFk;
5249+
}
5250+
51425251
public IEnumerable<ChildCompositeKey> ChildrenCompositeKey
51435252
{
51445253
get => Loader.Load(this, ref _childrenCompositeKey);
51455254
set => _childrenCompositeKey = value;
51465255
}
51475256

5257+
public async Task<IEnumerable<ChildCompositeKey>> LazyLoadChildrenCompositeKey(bool async)
5258+
{
5259+
if (async)
5260+
{
5261+
await Loader.LoadAsync(this, default, nameof(ChildrenCompositeKey));
5262+
return _childrenCompositeKey;
5263+
}
5264+
5265+
return ChildrenCompositeKey;
5266+
}
5267+
51485268
public SingleCompositeKey SingleCompositeKey
51495269
{
51505270
get => Loader.Load(this, ref _singleCompositeKey);
51515271
set => _singleCompositeKey = value;
51525272
}
5273+
5274+
public async Task<SingleCompositeKey> LazyLoadSingleCompositeKey(bool async)
5275+
{
5276+
if (async)
5277+
{
5278+
await Loader.LoadAsync(this, default, nameof(SingleCompositeKey));
5279+
return _singleCompositeKey;
5280+
}
5281+
5282+
return SingleCompositeKey;
5283+
}
51535284
}
51545285

51555286
protected class Child

0 commit comments

Comments
 (0)