Skip to content

Commit

Permalink
Most of explicit and lazy loading for no-tracking queries
Browse files Browse the repository at this point in the history
Part of #10042

Current limitations are:

- The lazy-loading delegate injection doesn't support tracking of when an navigation is loaded or not; I have an idea for this
- There is no identity resolution when the entities are not tracked, even if the query behavior is NoTrackingWithIdentityResolution
- The model should validated for only a single service property for a given type

I will submit PRs and/or file issues for these things.
  • Loading branch information
ajcvickers committed Dec 15, 2022
1 parent 3a62379 commit 0791ae6
Show file tree
Hide file tree
Showing 27 changed files with 9,364 additions and 3,963 deletions.
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
130 changes: 91 additions & 39 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,28 @@ 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 ??= new List<(object Entity, string NavigationName)>()).Contains(navEntry))
{
try
{
entry.Load();
_isLoading.Add(navEntry);
if (ShouldLoad(entity, navigationName, out var entry))
{
try
{
entry.Load();
}
catch
{
entry.IsLoaded = false;
throw;
}
}
}
catch
finally
{
SetLoaded(entity, navigationName, false);
throw;
_isLoading.Remove(navEntry);
}
}
}
Expand All @@ -103,53 +129,49 @@ 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 ??= new List<(object Entity, string NavigationName)>()).Contains(navEntry))
{
try
{
await entry.LoadAsync(cancellationToken).ConfigureAwait(false);
_isLoading.Add(navEntry);
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;
_isLoading.Remove(navEntry);
}
}
}

private bool ShouldLoad(object entity, string navigationName, [NotNullWhen(true)] out NavigationEntry? navigationEntry)
{
if (_loadedStates != null
&& _loadedStates.TryGetValue(navigationName, out var loaded)
&& loaded)
if (!_detached && !IsLoaded(entity, navigationName))
{
navigationEntry = null;
return false;
}

if (_disposed)
{
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);

if (entityEntry.State == EntityState.Detached)
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 = tempNavigationEntry;
navigationEntry = Context.Entry(entity).Navigation(navigationName); // Will use local-DetectChanges, if enabled.
if (!navigationEntry.IsLoaded)
{
Logger.NavigationLazyLoading(Context, entity, navigationName);

return true;
return true;
}
}
}

Expand All @@ -164,5 +186,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

0 comments on commit 0791ae6

Please sign in to comment.