Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Most of explicit and lazy loading for no-tracking queries #29865

Merged
merged 3 commits into from
Dec 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/EFCore.Abstractions/Infrastructure/ILazyLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ void SetLoaded(
[CallerMemberName] string navigationName = "",
bool loaded = true);

/// <summary>
/// Gets whether or not the given navigation as known to be completely loaded or known to be
/// no longer completely loaded.
/// </summary>
/// <param name="entity">The entity on which the navigation property is located.</param>
/// <param name="navigationName">The navigation property name.</param>
/// <returns><see langword="true" />if the navigation is known to be loaded.</returns>
bool IsLoaded(
object entity,
[CallerMemberName] string navigationName = "");

/// <summary>
/// Loads a navigation property if it has not already been loaded.
/// </summary>
Expand Down
54 changes: 35 additions & 19 deletions src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -474,22 +474,23 @@ private void SetServiceProperties(EntityState oldState, EntityState newState)
{
foreach (var serviceProperty in EntityType.GetServiceProperties())
{
this[serviceProperty]
= serviceProperty
this[serviceProperty] = (this[serviceProperty] is IInjectableService injectableService
? injectableService.Attaching(Context, Entity, injectableService)
: null)
?? serviceProperty
.ParameterBinding
.ServiceDelegate(
new MaterializationContext(
ValueBuffer.Empty,
Context),
EntityType,
Entity);
.ServiceDelegate(new MaterializationContext(ValueBuffer.Empty, Context), EntityType, Entity);
}
}
else if (newState == EntityState.Detached)
{
foreach (var serviceProperty in EntityType.GetServiceProperties())
{
this[serviceProperty] = null;
if (!(this[serviceProperty] is IInjectableService detachable)
|| detachable.Detaching(Context, Entity))
{
this[serviceProperty] = null;
}
}
}
}
Expand Down Expand Up @@ -846,7 +847,8 @@ private T ReadShadowValue<T>(int shadowIndex)
private static readonly MethodInfo ReadOriginalValueMethod
= typeof(InternalEntityEntry).GetTypeInfo().GetDeclaredMethod(nameof(ReadOriginalValue))!;

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060",
[UnconditionalSuppressMessage(
"ReflectionAnalysis", "IL2060",
Justification = "MakeGenericMethod wrapper, see https://github.com/dotnet/linker/issues/2482")]
internal static MethodInfo MakeReadOriginalValueMethod(Type type)
=> ReadOriginalValueMethod.MakeGenericMethod(type);
Expand All @@ -858,7 +860,8 @@ private T ReadOriginalValue<T>(IProperty property, int originalValueIndex)
private static readonly MethodInfo ReadRelationshipSnapshotValueMethod
= typeof(InternalEntityEntry).GetTypeInfo().GetDeclaredMethod(nameof(ReadRelationshipSnapshotValue))!;

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060",
[UnconditionalSuppressMessage(
"ReflectionAnalysis", "IL2060",
Justification = "MakeGenericMethod wrapper, see https://github.com/dotnet/linker/issues/2482")]
internal static MethodInfo MakeReadRelationshipSnapshotValueMethod(Type type)
=> ReadRelationshipSnapshotValueMethod.MakeGenericMethod(type);
Expand All @@ -867,7 +870,8 @@ internal static MethodInfo MakeReadRelationshipSnapshotValueMethod(Type type)
private T ReadRelationshipSnapshotValue<T>(IPropertyBase propertyBase, int relationshipSnapshotIndex)
=> _relationshipsSnapshot.GetValue<T>(this, propertyBase, relationshipSnapshotIndex);

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060",
[UnconditionalSuppressMessage(
"ReflectionAnalysis", "IL2060",
Justification = "MakeGenericMethod wrapper, see https://github.com/dotnet/linker/issues/2482")]
internal static MethodInfo MakeReadStoreGeneratedValueMethod(Type type)
=> ReadStoreGeneratedValueMethod.MakeGenericMethod(type);
Expand All @@ -882,7 +886,8 @@ private T ReadStoreGeneratedValue<T>(int storeGeneratedIndex)
private static readonly MethodInfo ReadTemporaryValueMethod
= typeof(InternalEntityEntry).GetTypeInfo().GetDeclaredMethod(nameof(ReadTemporaryValue))!;

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060",
[UnconditionalSuppressMessage(
"ReflectionAnalysis", "IL2060",
Justification = "MakeGenericMethod wrapper, see https://github.com/dotnet/linker/issues/2482")]
internal static MethodInfo MakeReadTemporaryValueMethod(Type type)
=> ReadTemporaryValueMethod.MakeGenericMethod(type);
Expand All @@ -895,7 +900,8 @@ private static readonly MethodInfo GetCurrentValueMethod
= typeof(InternalEntityEntry).GetTypeInfo().GetDeclaredMethods(nameof(GetCurrentValue)).Single(
m => m.IsGenericMethod);

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060",
[UnconditionalSuppressMessage(
"ReflectionAnalysis", "IL2060",
Justification = "MakeGenericMethod wrapper, see https://github.com/dotnet/linker/issues/2482")]
internal static MethodInfo MakeGetCurrentValueMethod(Type type)
=> GetCurrentValueMethod.MakeGenericMethod(type);
Expand Down Expand Up @@ -2001,11 +2007,14 @@ public void SetIsLoaded(INavigationBase navigation, bool loaded = true)
CoreStrings.ReferenceMustBeLoaded(navigation.Name, navigation.DeclaringEntityType.DisplayName()));
}

_stateData.FlagProperty(navigation.GetIndex(), PropertyFlag.IsLoaded, isFlagged: loaded);

foreach (var lazyLoaderProperty in EntityType.GetServiceProperties().Where(p => p.ClrType == typeof(ILazyLoader)))
var lazyLoader = GetLazyLoader();
if (lazyLoader != null)
{
((ILazyLoader?)this[lazyLoaderProperty])?.SetLoaded(Entity, navigation.Name, loaded);
lazyLoader.SetLoaded(Entity, navigation.Name, loaded);
}
else
{
_stateData.FlagProperty(navigation.GetIndex(), PropertyFlag.IsLoaded, isFlagged: loaded);
}
}

Expand All @@ -2016,7 +2025,14 @@ public void SetIsLoaded(INavigationBase navigation, bool loaded = true)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public bool IsLoaded(INavigationBase navigation)
=> _stateData.IsPropertyFlagged(navigation.GetIndex(), PropertyFlag.IsLoaded);
=> GetLazyLoader()?.IsLoaded(Entity, navigation.Name)
?? _stateData.IsPropertyFlagged(navigation.GetIndex(), PropertyFlag.IsLoaded);

private ILazyLoader? GetLazyLoader()
{
var lazyLoaderProperty = EntityType.GetServiceProperties().FirstOrDefault(p => p.ClrType == typeof(ILazyLoader));
return lazyLoaderProperty != null ? (ILazyLoader?)this[lazyLoaderProperty] : null;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
4 changes: 2 additions & 2 deletions src/EFCore/Diagnostics/CoreLoggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1108,7 +1108,7 @@ private static string ExecutionStrategyRetrying(EventDefinitionBase definition,
/// <param name="navigationName">The name of the navigation property.</param>
public static void LazyLoadOnDisposedContextWarning(
this IDiagnosticsLogger<DbLoggerCategory.Infrastructure> diagnostics,
DbContext context,
DbContext? context,
object entityType,
string navigationName)
{
Expand Down Expand Up @@ -1188,7 +1188,7 @@ private static string NavigationLazyLoading(EventDefinitionBase definition, Even
/// <param name="navigationName">The name of the navigation property.</param>
public static void DetachedLazyLoadingWarning(
this IDiagnosticsLogger<DbLoggerCategory.Infrastructure> diagnostics,
DbContext context,
DbContext? context,
object entityType,
string navigationName)
{
Expand Down
4 changes: 2 additions & 2 deletions src/EFCore/Diagnostics/LazyLoadingEventData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ public class LazyLoadingEventData : DbContextEventData
/// </summary>
/// <param name="eventDefinition">The event definition.</param>
/// <param name="messageGenerator">A delegate that generates a log message for this event.</param>
/// <param name="context">The current <see cref="DbContext" />.</param>
/// <param name="context">The current <see cref="DbContext" />, or <see langword="null" /> if it is no longer available.</param>
/// <param name="entity">The entity instance on which lazy loading was initiated.</param>
/// <param name="navigationPropertyName">The navigation property name of the relationship to be loaded.</param>
public LazyLoadingEventData(
EventDefinitionBase eventDefinition,
Func<EventDefinitionBase, EventData, string> messageGenerator,
DbContext context,
DbContext? context,
object entity,
string navigationPropertyName)
: base(eventDefinition, messageGenerator, context)
Expand Down
154 changes: 120 additions & 34 deletions src/EFCore/Infrastructure/Internal/LazyLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Microsoft.EntityFrameworkCore.Internal;

namespace Microsoft.EntityFrameworkCore.Infrastructure.Internal;

Expand All @@ -12,10 +13,12 @@ namespace Microsoft.EntityFrameworkCore.Infrastructure.Internal;
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public class LazyLoader : ILazyLoader
public class LazyLoader : ILazyLoader, IInjectableService
{
private bool _disposed;
private bool _detached;
private IDictionary<string, bool>? _loadedStates;
private List<(object Entity, string NavigationName)>? _isLoading;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -47,6 +50,17 @@ public virtual void SetLoaded(
_loadedStates[navigationName] = loaded;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual bool IsLoaded(object entity, string navigationName = "")
=> _loadedStates != null
&& _loadedStates.TryGetValue(navigationName, out var loaded)
&& loaded;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand All @@ -61,7 +75,7 @@ public virtual void SetLoaded(
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected virtual DbContext Context { get; }
protected virtual DbContext? Context { get; set; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -75,16 +89,29 @@ public virtual void Load(object entity, [CallerMemberName] string navigationName
Check.NotNull(entity, nameof(entity));
Check.NotEmpty(navigationName, nameof(navigationName));

if (ShouldLoad(entity, navigationName, out var entry))
var navEntry = (entity, navigationName);
if (!IsLoading(navEntry))
{
try
{
entry.Load();
_isLoading!.Add(navEntry);
// ShouldLoad is called after _isLoading.Add because it could attempt to load the property. See #13138.
if (ShouldLoad(entity, navigationName, out var entry))
{
try
{
entry.Load();
}
catch
{
entry.IsLoaded = false;
throw;
}
}
}
catch
finally
{
SetLoaded(entity, navigationName, false);
throw;
DoneLoading(navEntry);
}
}
}
Expand All @@ -103,53 +130,82 @@ public virtual async Task LoadAsync(
Check.NotNull(entity, nameof(entity));
Check.NotEmpty(navigationName, nameof(navigationName));

if (ShouldLoad(entity, navigationName, out var entry))
var navEntry = (entity, navigationName);
if (!IsLoading(navEntry))
{
try
{
await entry.LoadAsync(cancellationToken).ConfigureAwait(false);
_isLoading!.Add(navEntry);
// ShouldLoad is called after _isLoading.Add because it could attempt to load the property. See #13138.
if (ShouldLoad(entity, navigationName, out var entry))
{
try
{
await entry.LoadAsync(cancellationToken).ConfigureAwait(false);
}
catch
{
entry.IsLoaded = false;
throw;
}
}
}
catch
finally
{
SetLoaded(entity, navigationName, false);
throw;
DoneLoading(navEntry);
}
}
}

private bool ShouldLoad(object entity, string navigationName, [NotNullWhen(true)] out NavigationEntry? navigationEntry)
private bool IsLoading((object Entity, string NavigationName) navEntry)
=> (_isLoading ??= new List<(object Entity, string NavigationName)>())
.Contains(navEntry, EntityNavigationEqualityComparer.Instance);

private void DoneLoading((object Entity, string NavigationName) navEntry)
{
if (_loadedStates != null
&& _loadedStates.TryGetValue(navigationName, out var loaded)
&& loaded)
for (var i = 0; i < _isLoading!.Count; i++)
{
navigationEntry = null;
return false;
if (EntityNavigationEqualityComparer.Instance.Equals(navEntry, _isLoading[i]))
{
_isLoading.RemoveAt(i);
break;
}
}
}

if (_disposed)
private sealed class EntityNavigationEqualityComparer : IEqualityComparer<(object Entity, string NavigationName)>
{
public static readonly EntityNavigationEqualityComparer Instance = new();

private EntityNavigationEqualityComparer()
{
Logger.LazyLoadOnDisposedContextWarning(Context, entity, navigationName);
}
else if (Context.ChangeTracker.LazyLoadingEnabled)
{
// Set early to avoid recursive loading overflow
SetLoaded(entity, navigationName, loaded: true);

var entityEntry = Context.Entry(entity); // Will use local-DetectChanges, if enabled.
var tempNavigationEntry = entityEntry.Navigation(navigationName);
public bool Equals((object Entity, string NavigationName) x, (object Entity, string NavigationName) y)
=> ReferenceEquals(x.Entity, y.Entity)
&& string.Equals(x.NavigationName, y.NavigationName, StringComparison.Ordinal);

if (entityEntry.State == EntityState.Detached)
public int GetHashCode((object Entity, string NavigationName) obj)
=> HashCode.Combine(obj.Entity.GetHashCode(), obj.GetHashCode());
}

private bool ShouldLoad(object entity, string navigationName, [NotNullWhen(true)] out NavigationEntry? navigationEntry)
{
if (!_detached && !IsLoaded(entity, navigationName))
{
if (_disposed)
{
Logger.DetachedLazyLoadingWarning(Context, entity, navigationName);
Logger.LazyLoadOnDisposedContextWarning(Context, entity, navigationName);
}
else if (!tempNavigationEntry.IsLoaded)
else if (Context!.ChangeTracker.LazyLoadingEnabled) // Check again because the nav may be loaded without the loader knowing
{
Logger.NavigationLazyLoading(Context, entity, navigationName);
navigationEntry = Context.Entry(entity).Navigation(navigationName); // Will use local-DetectChanges, if enabled.
if (!navigationEntry.IsLoaded)
{
Logger.NavigationLazyLoading(Context, entity, navigationName);

navigationEntry = tempNavigationEntry;

return true;
return true;
}
}
}

Expand All @@ -164,5 +220,35 @@ private bool ShouldLoad(object entity, string navigationName, [NotNullWhen(true)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual void Dispose()
=> _disposed = true;
{
Context = null;
_disposed = true;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual bool Detaching(DbContext context, object entity)
{
_detached = true;
Dispose();
return false;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual IInjectableService? Attaching(DbContext context, object entity, IInjectableService? existingService)
{
_disposed = false;
_detached = false;
Context = context;
return this;
}
}
Loading