Skip to content

Commit

Permalink
Explicit and lazy loading for many-to-many collections (#22023)
Browse files Browse the repository at this point in the history
Part of #19003 and #10508
  • Loading branch information
ajcvickers authored Aug 12, 2020
1 parent f69b5ac commit ca57b8e
Show file tree
Hide file tree
Showing 23 changed files with 1,558 additions and 140 deletions.
22 changes: 19 additions & 3 deletions src/EFCore/ChangeTracking/CollectionEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking
/// </summary>
public class CollectionEntry : NavigationEntry
{
private ICollectionLoader _loader;

/// <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 Down Expand Up @@ -59,10 +61,12 @@ private void LocalDetectChanges()
{
var targetType = Metadata.TargetEntityType;
var context = InternalEntry.StateManager.Context;

var changeDetector = context.ChangeTracker.AutoDetectChangesEnabled
&& (string)context.Model[CoreAnnotationNames.SkipDetectChangesAnnotation] != "true"
? context.GetDependencies().ChangeDetector
: null;

foreach (var entity in collection.OfType<object>().ToList())
{
var entry = InternalEntry.StateManager.GetOrCreateEntry(entity, targetType);
Expand Down Expand Up @@ -200,7 +204,10 @@ public override void Load()
{
EnsureInitialized();

base.Load();
if (!IsLoaded)
{
TargetLoader.Load(InternalEntry);
}
}

/// <summary>
Expand All @@ -226,7 +233,9 @@ public override Task LoadAsync(CancellationToken cancellationToken = default)
{
EnsureInitialized();

return base.LoadAsync(cancellationToken);
return IsLoaded
? Task.CompletedTask
: TargetLoader.LoadAsync(InternalEntry, cancellationToken);
}

/// <summary>
Expand All @@ -243,7 +252,7 @@ public override IQueryable Query()
{
EnsureInitialized();

return base.Query();
return TargetLoader.Query(InternalEntry);
}

private void EnsureInitialized()
Expand Down Expand Up @@ -274,5 +283,12 @@ protected virtual InternalEntityEntry GetInternalTargetEntry([NotNull] object en
|| !Metadata.GetCollectionAccessor().Contains(InternalEntry.Entity, entity)
? null
: InternalEntry.StateManager.GetOrCreateEntry(entity, Metadata.TargetEntityType);

private ICollectionLoader TargetLoader
=> _loader ??= Metadata is ISkipNavigation skipNavigation
? skipNavigation.GetManyToManyLoader()
: new EntityFinderCollectionLoaderAdapter(
InternalEntry.StateManager.CreateEntityFinder(Metadata.TargetEntityType),
(INavigation)Metadata);
}
}
3 changes: 2 additions & 1 deletion src/EFCore/ChangeTracking/EntityEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking
public class EntityEntry : IInfrastructure<InternalEntityEntry>
{
private static readonly int _maxEntityState = Enum.GetValues(typeof(EntityState)).Cast<int>().Max();
private IEntityFinder _finder;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -413,7 +414,7 @@ private void Reload(PropertyValues storeValues)
}

private IEntityFinder Finder
=> InternalEntry.StateManager.CreateEntityFinder(InternalEntry.EntityType);
=> _finder ??= InternalEntry.StateManager.CreateEntityFinder(InternalEntry.EntityType);

/// <summary>
/// Returns a string that represents the current object.
Expand Down
20 changes: 3 additions & 17 deletions src/EFCore/ChangeTracking/NavigationEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Microsoft.EntityFrameworkCore.ChangeTracking
Expand Down Expand Up @@ -100,13 +99,7 @@ private static INavigationBase GetNavigation(InternalEntityEntry internalEntry,
/// Note that entities that are already being tracked are not overwritten with new data from the database.
/// </para>
/// </summary>
public virtual void Load()
{
if (!IsLoaded)
{
TargetFinder.Load(Metadata, InternalEntry);
}
}
public abstract void Load();

/// <summary>
/// <para>
Expand All @@ -127,10 +120,7 @@ public virtual void Load()
/// <returns>
/// A task that represents the asynchronous operation.
/// </returns>
public virtual Task LoadAsync(CancellationToken cancellationToken = default)
=> IsLoaded
? Task.CompletedTask
: TargetFinder.LoadAsync(Metadata, InternalEntry, cancellationToken);
public abstract Task LoadAsync(CancellationToken cancellationToken = default);

/// <summary>
/// <para>
Expand All @@ -143,8 +133,7 @@ public virtual Task LoadAsync(CancellationToken cancellationToken = default)
/// </para>
/// </summary>
/// <returns> The query to load related entities. </returns>
public virtual IQueryable Query()
=> TargetFinder.Query(Metadata, InternalEntry);
public abstract IQueryable Query();

/// <summary>
/// <para>
Expand Down Expand Up @@ -175,9 +164,6 @@ public virtual bool IsLoaded
set => InternalEntry.SetIsLoaded(Metadata, value);
}

private IEntityFinder TargetFinder
=> InternalEntry.StateManager.CreateEntityFinder(Metadata.TargetEntityType);

/// <summary>
/// Gets the metadata that describes the facets of this property and how it maps to the database.
/// </summary>
Expand Down
62 changes: 62 additions & 0 deletions src/EFCore/ChangeTracking/ReferenceEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;
Expand All @@ -24,6 +26,8 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking
/// </summary>
public class ReferenceEntry : NavigationEntry
{
private IEntityFinder _finder;

/// <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 Down Expand Up @@ -74,6 +78,61 @@ private void LocalDetectChanges()
}
}

/// <summary>
/// <para>
/// Loads the entity or entities referenced by this navigation property, unless <see cref="NavigationEntry.IsLoaded" />
/// is already set to true.
/// </para>
/// <para>
/// Note that entities that are already being tracked are not overwritten with new data from the database.
/// </para>
/// </summary>
public override void Load()
{
if (!IsLoaded)
{
TargetFinder.Load((INavigation)Metadata, InternalEntry);
}
}

/// <summary>
/// <para>
/// Loads the entity or entities referenced by this navigation property, unless <see cref="NavigationEntry.IsLoaded" />
/// is already set to true.
/// </para>
/// <para>
/// Note that entities that are already being tracked are not overwritten with new data from the database.
/// </para>
/// <para>
/// Multiple active operations on the same context instance are not supported. Use 'await' to ensure
/// that any asynchronous operations have completed before calling another method on this context.
/// </para>
/// </summary>
/// <param name="cancellationToken">
/// A <see cref="CancellationToken" /> to observe while waiting for the task to complete.
/// </param>
/// <returns>
/// A task that represents the asynchronous operation.
/// </returns>
public override Task LoadAsync(CancellationToken cancellationToken = default)
=> IsLoaded
? Task.CompletedTask
: TargetFinder.LoadAsync((INavigation)Metadata, InternalEntry, cancellationToken);

/// <summary>
/// <para>
/// Returns the query that would be used by <see cref="Load" /> to load entities referenced by
/// this navigation property.
/// </para>
/// <para>
/// The query can be composed over using LINQ to perform filtering, counting, etc. without
/// actually loading all entities from the database.
/// </para>
/// </summary>
/// <returns> The query to load related entities. </returns>
public override IQueryable Query()
=> TargetFinder.Query((INavigation)Metadata, InternalEntry);

/// <summary>
/// Gets or sets a value indicating whether any of foreign key property values associated
/// with this navigation property have been modified and should be updated in the database
Expand Down Expand Up @@ -167,5 +226,8 @@ protected virtual InternalEntityEntry GetTargetEntry()
=> CurrentValue == null
? null
: InternalEntry.StateManager.GetOrCreateEntry(CurrentValue, Metadata.TargetEntityType);

private IEntityFinder TargetFinder
=> _finder ??= InternalEntry.StateManager.CreateEntityFinder(Metadata.TargetEntityType);
}
}
57 changes: 57 additions & 0 deletions src/EFCore/Extensions/Internal/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Utilities;

// ReSharper disable once CheckNamespace
Expand Down Expand Up @@ -196,5 +197,61 @@ private static Expression RemoveConvert(Expression expression)

return expression;
}

private static readonly MethodInfo _objectEqualsMethodInfo
= typeof(object).GetRuntimeMethod(nameof(object.Equals), new[] { typeof(object), typeof(object) });

/// <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 static Expression BuildPredicate(
[NotNull] IReadOnlyList<IProperty> keyProperties,
ValueBuffer keyValues,
[NotNull] ParameterExpression entityParameter)
{
var keyValuesConstant = Expression.Constant(keyValues);

var predicate = GenerateEqualExpression(entityParameter, keyValuesConstant, keyProperties[0], 0);

for (var i = 1; i < keyProperties.Count; i++)
{
predicate = Expression.AndAlso(predicate, GenerateEqualExpression(entityParameter, keyValuesConstant, keyProperties[i], i));
}

return predicate;

static Expression GenerateEqualExpression(
Expression entityParameterExpression, Expression keyValuesConstantExpression, IProperty property, int i)
=> property.ClrType.IsValueType
&& property.ClrType.UnwrapNullableType() is Type nonNullableType
&& !(nonNullableType == typeof(bool) || nonNullableType.IsNumeric() || nonNullableType.IsEnum)
? Expression.Call(
_objectEqualsMethodInfo,
Expression.Call(
EF.PropertyMethod.MakeGenericMethod(typeof(object)),
entityParameterExpression,
Expression.Constant(property.Name, typeof(string))),
Expression.Convert(
Expression.Call(
keyValuesConstantExpression,
ValueBuffer.GetValueMethod,
Expression.Constant(i)),
typeof(object)))
: (Expression)Expression.Equal(
Expression.Call(
EF.PropertyMethod.MakeGenericMethod(property.ClrType),
entityParameterExpression,
Expression.Constant(property.Name, typeof(string))),
Expression.Convert(
Expression.Call(
keyValuesConstantExpression,
ValueBuffer.GetValueMethod,
Expression.Constant(i)),
property.ClrType));
}

}
}
Loading

0 comments on commit ca57b8e

Please sign in to comment.