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

Explicit and lazy loading for many-to-many collections #22023

Merged
merged 1 commit into from
Aug 12, 2020
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
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