From 2c5f0544939f313aab9ced18818ffd6b916501df Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Mon, 10 Aug 2020 08:46:48 -0700 Subject: [PATCH] Explicit and lazy loading for many-to-many collections Part of #19003 and #10508 --- src/EFCore/ChangeTracking/CollectionEntry.cs | 22 +- src/EFCore/ChangeTracking/EntityEntry.cs | 3 +- src/EFCore/ChangeTracking/NavigationEntry.cs | 20 +- src/EFCore/ChangeTracking/ReferenceEntry.cs | 62 ++ .../Internal/ExpressionExtensions.cs | 57 ++ src/EFCore/Internal/EntityFinder.cs | 83 +- .../EntityFinderCollectionLoaderAdapter.cs | 63 ++ src/EFCore/Internal/ICollectionLoader.cs | 44 ++ src/EFCore/Internal/ICollectionLoader`.cs | 27 + src/EFCore/Internal/IEntityFinder.cs | 6 +- src/EFCore/Internal/IEntityFinder`.cs | 2 +- src/EFCore/Internal/ManyToManyLoader.cs | 203 +++++ .../Internal/ManyToManyLoaderFactory.cs | 39 + .../Metadata/Internal/NavigationExtensions.cs | 10 + .../Metadata/Internal/SkipNavigation.cs | 11 + .../ManyToManyLoadInMemoryTest.cs | 20 + .../LoadTestBase.cs | 65 +- .../ManyToManyLoadTestBase.cs | 746 ++++++++++++++++++ .../LoadSqlServerTest.cs | 4 +- .../ManyToManyLoadSqlServerTest.cs | 121 +++ .../ManyToManyLoadProxySqliteTest.cs | 29 + .../ManyToManyLoadSqliteTest.cs | 18 + .../ManyToManyLoadSqliteTestBase.cs | 43 + 23 files changed, 1558 insertions(+), 140 deletions(-) create mode 100644 src/EFCore/Internal/EntityFinderCollectionLoaderAdapter.cs create mode 100644 src/EFCore/Internal/ICollectionLoader.cs create mode 100644 src/EFCore/Internal/ICollectionLoader`.cs create mode 100644 src/EFCore/Internal/ManyToManyLoader.cs create mode 100644 src/EFCore/Internal/ManyToManyLoaderFactory.cs create mode 100644 test/EFCore.InMemory.FunctionalTests/ManyToManyLoadInMemoryTest.cs create mode 100644 test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/ManyToManyLoadSqlServerTest.cs create mode 100644 test/EFCore.Sqlite.FunctionalTests/ManyToManyLoadProxySqliteTest.cs create mode 100644 test/EFCore.Sqlite.FunctionalTests/ManyToManyLoadSqliteTest.cs create mode 100644 test/EFCore.Sqlite.FunctionalTests/ManyToManyLoadSqliteTestBase.cs diff --git a/src/EFCore/ChangeTracking/CollectionEntry.cs b/src/EFCore/ChangeTracking/CollectionEntry.cs index 7bb46345ca2..c27e6332479 100644 --- a/src/EFCore/ChangeTracking/CollectionEntry.cs +++ b/src/EFCore/ChangeTracking/CollectionEntry.cs @@ -26,6 +26,8 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking /// public class CollectionEntry : NavigationEntry { + private ICollectionLoader _loader; + /// /// 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 @@ -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().ToList()) { var entry = InternalEntry.StateManager.GetOrCreateEntry(entity, targetType); @@ -200,7 +204,10 @@ public override void Load() { EnsureInitialized(); - base.Load(); + if (!IsLoaded) + { + TargetLoader.Load(InternalEntry); + } } /// @@ -226,7 +233,9 @@ public override Task LoadAsync(CancellationToken cancellationToken = default) { EnsureInitialized(); - return base.LoadAsync(cancellationToken); + return IsLoaded + ? Task.CompletedTask + : TargetLoader.LoadAsync(InternalEntry, cancellationToken); } /// @@ -243,7 +252,7 @@ public override IQueryable Query() { EnsureInitialized(); - return base.Query(); + return TargetLoader.Query(InternalEntry); } private void EnsureInitialized() @@ -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); } } diff --git a/src/EFCore/ChangeTracking/EntityEntry.cs b/src/EFCore/ChangeTracking/EntityEntry.cs index 8a7d3d6a801..7c975caa4e2 100644 --- a/src/EFCore/ChangeTracking/EntityEntry.cs +++ b/src/EFCore/ChangeTracking/EntityEntry.cs @@ -33,6 +33,7 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking public class EntityEntry : IInfrastructure { private static readonly int _maxEntityState = Enum.GetValues(typeof(EntityState)).Cast().Max(); + private IEntityFinder _finder; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -413,7 +414,7 @@ private void Reload(PropertyValues storeValues) } private IEntityFinder Finder - => InternalEntry.StateManager.CreateEntityFinder(InternalEntry.EntityType); + => _finder ??= InternalEntry.StateManager.CreateEntityFinder(InternalEntry.EntityType); /// /// Returns a string that represents the current object. diff --git a/src/EFCore/ChangeTracking/NavigationEntry.cs b/src/EFCore/ChangeTracking/NavigationEntry.cs index 6aa906e3403..0613bdd77e7 100644 --- a/src/EFCore/ChangeTracking/NavigationEntry.cs +++ b/src/EFCore/ChangeTracking/NavigationEntry.cs @@ -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 @@ -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. /// /// - public virtual void Load() - { - if (!IsLoaded) - { - TargetFinder.Load(Metadata, InternalEntry); - } - } + public abstract void Load(); /// /// @@ -127,10 +120,7 @@ public virtual void Load() /// /// A task that represents the asynchronous operation. /// - public virtual Task LoadAsync(CancellationToken cancellationToken = default) - => IsLoaded - ? Task.CompletedTask - : TargetFinder.LoadAsync(Metadata, InternalEntry, cancellationToken); + public abstract Task LoadAsync(CancellationToken cancellationToken = default); /// /// @@ -143,8 +133,7 @@ public virtual Task LoadAsync(CancellationToken cancellationToken = default) /// /// /// The query to load related entities. - public virtual IQueryable Query() - => TargetFinder.Query(Metadata, InternalEntry); + public abstract IQueryable Query(); /// /// @@ -175,9 +164,6 @@ public virtual bool IsLoaded set => InternalEntry.SetIsLoaded(Metadata, value); } - private IEntityFinder TargetFinder - => InternalEntry.StateManager.CreateEntityFinder(Metadata.TargetEntityType); - /// /// Gets the metadata that describes the facets of this property and how it maps to the database. /// diff --git a/src/EFCore/ChangeTracking/ReferenceEntry.cs b/src/EFCore/ChangeTracking/ReferenceEntry.cs index 0ce542d8bd2..6c5f59a9b3a 100644 --- a/src/EFCore/ChangeTracking/ReferenceEntry.cs +++ b/src/EFCore/ChangeTracking/ReferenceEntry.cs @@ -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; @@ -24,6 +26,8 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking /// public class ReferenceEntry : NavigationEntry { + private IEntityFinder _finder; + /// /// 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 @@ -74,6 +78,61 @@ private void LocalDetectChanges() } } + /// + /// + /// Loads the entity or entities referenced by this navigation property, unless + /// is already set to true. + /// + /// + /// Note that entities that are already being tracked are not overwritten with new data from the database. + /// + /// + public override void Load() + { + if (!IsLoaded) + { + TargetFinder.Load((INavigation)Metadata, InternalEntry); + } + } + + /// + /// + /// Loads the entity or entities referenced by this navigation property, unless + /// is already set to true. + /// + /// + /// Note that entities that are already being tracked are not overwritten with new data from the database. + /// + /// + /// 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. + /// + /// + /// + /// A to observe while waiting for the task to complete. + /// + /// + /// A task that represents the asynchronous operation. + /// + public override Task LoadAsync(CancellationToken cancellationToken = default) + => IsLoaded + ? Task.CompletedTask + : TargetFinder.LoadAsync((INavigation)Metadata, InternalEntry, cancellationToken); + + /// + /// + /// Returns the query that would be used by to load entities referenced by + /// this navigation property. + /// + /// + /// The query can be composed over using LINQ to perform filtering, counting, etc. without + /// actually loading all entities from the database. + /// + /// + /// The query to load related entities. + public override IQueryable Query() + => TargetFinder.Query((INavigation)Metadata, InternalEntry); + /// /// 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 @@ -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); } } diff --git a/src/EFCore/Extensions/Internal/ExpressionExtensions.cs b/src/EFCore/Extensions/Internal/ExpressionExtensions.cs index 87b87949e67..fb35693710d 100644 --- a/src/EFCore/Extensions/Internal/ExpressionExtensions.cs +++ b/src/EFCore/Extensions/Internal/ExpressionExtensions.cs @@ -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 @@ -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) }); + + /// + /// 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. + /// + public static Expression BuildPredicate( + [NotNull] IReadOnlyList 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)); + } + } } diff --git a/src/EFCore/Internal/EntityFinder.cs b/src/EFCore/Internal/EntityFinder.cs index fc66d8a1a41..3c9100def1c 100644 --- a/src/EFCore/Internal/EntityFinder.cs +++ b/src/EFCore/Internal/EntityFinder.cs @@ -26,9 +26,6 @@ namespace Microsoft.EntityFrameworkCore.Internal public class EntityFinder : IEntityFinder where TEntity : class { - private static readonly MethodInfo _objectEqualsMethodInfo - = typeof(object).GetRuntimeMethod(nameof(object.Equals), new[] { typeof(object), typeof(object) }); - private readonly IStateManager _stateManager; private readonly IDbSetSource _setSource; private readonly IDbSetCache _setCache; @@ -61,12 +58,10 @@ public EntityFinder( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual TEntity Find(object[] keyValues) - { - return keyValues == null || keyValues.Any(v => v == null) + => keyValues == null || keyValues.Any(v => v == null) ? null : (FindTracked(keyValues, out var keyProperties) ?? _queryRoot.FirstOrDefault(BuildLambda(keyProperties, new ValueBuffer(keyValues)))); - } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -126,7 +121,7 @@ ValueTask IEntityFinder.FindAsync(object[] keyValues, CancellationToken /// 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. /// - public virtual void Load(INavigationBase navigation, InternalEntityEntry entry) + public virtual void Load(INavigation navigation, InternalEntityEntry entry) { if (entry.EntityState == EntityState.Detached) { @@ -150,7 +145,7 @@ public virtual void Load(INavigationBase navigation, InternalEntityEntry entry) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual async Task LoadAsync( - INavigationBase navigation, + INavigation navigation, InternalEntityEntry entry, CancellationToken cancellationToken = default) { @@ -176,7 +171,7 @@ await Query(navigation, keyValues).LoadAsync(cancellationToken) /// 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. /// - public virtual IQueryable Query(INavigationBase navigation, InternalEntityEntry entry) + public virtual IQueryable Query(INavigation navigation, InternalEntityEntry entry) { if (entry.EntityState == EntityState.Detached) { @@ -236,7 +231,7 @@ private IQueryable GetDatabaseValuesQuery(InternalEntityEntry entry) .Select(BuildProjection(entityType)); } - private IQueryable Query(INavigationBase navigation, object[] keyValues) + private IQueryable Query(INavigation navigation, object[] keyValues) => _queryRoot.Where(BuildLambda(GetLoadProperties(navigation), new ValueBuffer(keyValues))).AsTracking(); /// @@ -245,14 +240,14 @@ private IQueryable Query(INavigationBase navigation, object[] keyValues /// 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. /// - IQueryable IEntityFinder.Query(INavigationBase navigation, InternalEntityEntry entry) + IQueryable IEntityFinder.Query(INavigation navigation, InternalEntityEntry entry) => Query(navigation, entry); - private static object[] GetLoadValues(INavigationBase navigation, InternalEntityEntry entry) + private static object[] GetLoadValues(INavigation navigation, InternalEntityEntry entry) { - var properties = ((INavigation)navigation).IsOnDependent - ? ((INavigation)navigation).ForeignKey.Properties - : ((INavigation)navigation).ForeignKey.PrincipalKey.Properties; + var properties = navigation.IsOnDependent + ? navigation.ForeignKey.Properties + : navigation.ForeignKey.PrincipalKey.Properties; var values = new object[properties.Count]; @@ -270,10 +265,10 @@ private static object[] GetLoadValues(INavigationBase navigation, InternalEntity return values; } - private static IReadOnlyList GetLoadProperties(INavigationBase navigation) - => ((INavigation)navigation).IsOnDependent - ? ((INavigation)navigation).ForeignKey.PrincipalKey.Properties - : ((INavigation)navigation).ForeignKey.Properties; + private static IReadOnlyList GetLoadProperties(INavigation navigation) + => navigation.IsOnDependent + ? navigation.ForeignKey.PrincipalKey.Properties + : navigation.ForeignKey.Properties; private TEntity FindTracked(object[] keyValues, out IReadOnlyList keyProperties) { @@ -312,7 +307,7 @@ private static Expression> BuildLambda(IReadOnlyList>( - BuildPredicate(keyProperties, keyValues, entityParameter), entityParameter); + ExpressionExtensions.BuildPredicate(keyProperties, keyValues, entityParameter), entityParameter); } private static Expression> BuildObjectLambda(IReadOnlyList keyProperties, ValueBuffer keyValues) @@ -320,7 +315,7 @@ private static Expression> BuildObjectLambda(IReadOnlyList>( - BuildPredicate(keyProperties, keyValues, entityParameter), entityParameter); + ExpressionExtensions.BuildPredicate(keyProperties, keyValues, entityParameter), entityParameter); } private IQueryable BuildQueryRoot(IEntityType entityType) @@ -357,52 +352,6 @@ private static IQueryable Select( parameter)); } - private static Expression BuildPredicate( - IReadOnlyList keyProperties, - ValueBuffer keyValues, - 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)); - } - private static Expression> BuildProjection(IEntityType entityType) { var entityParameter = Expression.Parameter(typeof(object), "e"); diff --git a/src/EFCore/Internal/EntityFinderCollectionLoaderAdapter.cs b/src/EFCore/Internal/EntityFinderCollectionLoaderAdapter.cs new file mode 100644 index 00000000000..074f035c8ea --- /dev/null +++ b/src/EFCore/Internal/EntityFinderCollectionLoaderAdapter.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// 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.Metadata; + +namespace Microsoft.EntityFrameworkCore.Internal +{ + /// + /// 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. + /// + public class EntityFinderCollectionLoaderAdapter : ICollectionLoader + { + private readonly IEntityFinder _entityFinder; + private readonly INavigation _navigation; + + /// + /// 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. + /// + public EntityFinderCollectionLoaderAdapter([NotNull] IEntityFinder entityFinder, [NotNull] INavigation navigation) + { + _entityFinder = entityFinder; + _navigation = navigation; + } + + /// + /// 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. + /// + public virtual void Load(InternalEntityEntry entry) + => _entityFinder.Load(_navigation, entry); + + /// + /// 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. + /// + public virtual Task LoadAsync(InternalEntityEntry entry, CancellationToken cancellationToken = default) + => _entityFinder.LoadAsync(_navigation, entry, cancellationToken); + + /// + /// 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. + /// + public virtual IQueryable Query(InternalEntityEntry entry) + => _entityFinder.Query(_navigation, entry); + } +} diff --git a/src/EFCore/Internal/ICollectionLoader.cs b/src/EFCore/Internal/ICollectionLoader.cs new file mode 100644 index 00000000000..87a77a59ec3 --- /dev/null +++ b/src/EFCore/Internal/ICollectionLoader.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// 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; + +namespace Microsoft.EntityFrameworkCore.Internal +{ + /// + /// 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. + /// + public interface ICollectionLoader + { + /// + /// 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. + /// + void Load([NotNull] InternalEntityEntry entry); + + /// + /// 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. + /// + Task LoadAsync([NotNull] InternalEntityEntry entry, CancellationToken cancellationToken = default); + + /// + /// 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. + /// + IQueryable Query([NotNull] InternalEntityEntry entry); + } +} diff --git a/src/EFCore/Internal/ICollectionLoader`.cs b/src/EFCore/Internal/ICollectionLoader`.cs new file mode 100644 index 00000000000..0759e82da04 --- /dev/null +++ b/src/EFCore/Internal/ICollectionLoader`.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; + +namespace Microsoft.EntityFrameworkCore.Internal +{ + /// + /// 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. + /// + public interface ICollectionLoader : ICollectionLoader + where TEntity : class + { + /// + /// 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. + /// + new IQueryable Query([NotNull] InternalEntityEntry entry); + } +} diff --git a/src/EFCore/Internal/IEntityFinder.cs b/src/EFCore/Internal/IEntityFinder.cs index fcb06c0e238..c0d3ae252de 100644 --- a/src/EFCore/Internal/IEntityFinder.cs +++ b/src/EFCore/Internal/IEntityFinder.cs @@ -40,7 +40,7 @@ public interface IEntityFinder /// 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. /// - void Load([NotNull] INavigationBase navigation, [NotNull] InternalEntityEntry entry); + void Load([NotNull] INavigation navigation, [NotNull] InternalEntityEntry entry); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -49,7 +49,7 @@ public interface IEntityFinder /// doing so can result in application failures when updating to a new Entity Framework Core release. /// Task LoadAsync( - [NotNull] INavigationBase navigation, + [NotNull] INavigation navigation, [NotNull] InternalEntityEntry entry, CancellationToken cancellationToken = default); @@ -59,7 +59,7 @@ Task LoadAsync( /// 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. /// - IQueryable Query([NotNull] INavigationBase navigation, [NotNull] InternalEntityEntry entry); + IQueryable Query([NotNull] INavigation navigation, [NotNull] InternalEntityEntry entry); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Internal/IEntityFinder`.cs b/src/EFCore/Internal/IEntityFinder`.cs index 865760874be..47ce5c27e6e 100644 --- a/src/EFCore/Internal/IEntityFinder`.cs +++ b/src/EFCore/Internal/IEntityFinder`.cs @@ -41,6 +41,6 @@ public interface IEntityFinder : IEntityFinder /// 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. /// - new IQueryable Query([NotNull] INavigationBase navigation, [NotNull] InternalEntityEntry entry); + new IQueryable Query([NotNull] INavigation navigation, [NotNull] InternalEntityEntry entry); } } diff --git a/src/EFCore/Internal/ManyToManyLoader.cs b/src/EFCore/Internal/ManyToManyLoader.cs new file mode 100644 index 00000000000..76d1417ef3f --- /dev/null +++ b/src/EFCore/Internal/ManyToManyLoader.cs @@ -0,0 +1,203 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Internal +{ + /// + /// 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. + /// + public class ManyToManyLoader : ICollectionLoader + where TEntity : class + where TSourceEntity : class + { + private readonly ISkipNavigation _skipNavigation; + + /// + /// 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. + /// + public ManyToManyLoader([NotNull] ISkipNavigation skipNavigation) + { + _skipNavigation = skipNavigation; + } + + /// + /// 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. + /// + public virtual void Load(InternalEntityEntry entry) + { + var keyValues = PrepareForLoad(entry); + + // Short-circuit for any null key values for perf and because of #6129 + if (keyValues != null) + { + Query(entry.StateManager.Context, keyValues).Load(); + } + + entry.SetIsLoaded(_skipNavigation); + } + + /// + /// 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. + /// + public virtual async Task LoadAsync(InternalEntityEntry entry, CancellationToken cancellationToken = default) + { + var keyValues = PrepareForLoad(entry); + + // Short-circuit for any null key values for perf and because of #6129 + if (keyValues != null) + { + await Query(entry.StateManager.Context, keyValues).LoadAsync(cancellationToken).ConfigureAwait(false); + } + + entry.SetIsLoaded(_skipNavigation); + } + + /// + /// 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. + /// + public virtual IQueryable Query(InternalEntityEntry entry) + { + var keyValues = PrepareForLoad(entry); + var context = entry.StateManager.Context; + + // Short-circuit for any null key values for perf and because of #6129 + if (keyValues == null) + { + // Creates an empty Queryable that works with Async. Has to be an EF query because it could be used in a composition. + var queryRoot = _skipNavigation.TargetEntityType.HasSharedClrType + ? context.Set(_skipNavigation.TargetEntityType.Name) + : context.Set(); + + return queryRoot.Where(e => false); + } + + return Query(context, keyValues); + } + + private object[] PrepareForLoad(InternalEntityEntry entry) + { + if (entry.EntityState == EntityState.Detached) + { + throw new InvalidOperationException(CoreStrings.CannotLoadDetached(_skipNavigation.Name, entry.EntityType.DisplayName())); + } + + var properties = _skipNavigation.ForeignKey.PrincipalKey.Properties; + var values = new object[properties.Count]; + + for (var i = 0; i < values.Length; i++) + { + var value = entry[properties[i]]; + if (value == null) + { + return null; + } + + values[i] = value; + } + + return values; + } + + /// + /// 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. + /// + IQueryable ICollectionLoader.Query(InternalEntityEntry entry) + => Query(entry); + + private IQueryable Query( + DbContext context, + object[] keyValues) + { + var loadProperties = _skipNavigation.ForeignKey.PrincipalKey.Properties; + + // Example of query being built: + // + // IQueryable loaded + // = context.Set() + // .AsTracking() + // .Where(e => e.Id == left.Id) + // .SelectMany(e => e.TwoSkip) + // .Include(e => e.OneSkip.Where(e => e.Id == left.Id)); + + var queryRoot = _skipNavigation.DeclaringEntityType.HasSharedClrType + ? context.Set(_skipNavigation.DeclaringEntityType.Name) + : context.Set(); + + return queryRoot + .AsTracking() + .Where(BuildWhereLambda(loadProperties, new ValueBuffer(keyValues))) + .SelectMany(BuildSelectManyLambda(_skipNavigation)) + .Include(BuildIncludeLambda(_skipNavigation.Inverse, loadProperties, new ValueBuffer(keyValues))) + .AsQueryable(); + } + + private static Expression>> BuildIncludeLambda( + ISkipNavigation skipNavigation, + IReadOnlyList keyProperties, + ValueBuffer keyValues) + { + var whereParameter = Expression.Parameter(typeof(TSourceEntity), "e"); + var entityParameter = Expression.Parameter(typeof(TEntity), "e"); + + return Expression.Lambda>>( + Expression.Call( + EnumerableMethods.Where.MakeGenericMethod(typeof(TSourceEntity)), + Expression.MakeMemberAccess( + entityParameter, + skipNavigation.PropertyInfo), + Expression.Lambda>( + ExpressionExtensions.BuildPredicate(keyProperties, keyValues, whereParameter), + whereParameter)), entityParameter); + } + + private static Expression> BuildWhereLambda( + IReadOnlyList keyProperties, ValueBuffer keyValues) + { + var entityParameter = Expression.Parameter(typeof(TSourceEntity), "e"); + + return Expression.Lambda>( + ExpressionExtensions.BuildPredicate(keyProperties, keyValues, entityParameter), entityParameter); + } + + private static Expression>> BuildSelectManyLambda(INavigationBase navigation) + { + var entityParameter = Expression.Parameter(typeof(TSourceEntity), "e"); + + return Expression.Lambda>>( + Expression.MakeMemberAccess( + entityParameter, + navigation.PropertyInfo), + entityParameter); + } + } +} diff --git a/src/EFCore/Internal/ManyToManyLoaderFactory.cs b/src/EFCore/Internal/ManyToManyLoaderFactory.cs new file mode 100644 index 00000000000..55cb3f6cb00 --- /dev/null +++ b/src/EFCore/Internal/ManyToManyLoaderFactory.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Microsoft.EntityFrameworkCore.Internal +{ + /// + /// 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. + /// + public class ManyToManyLoaderFactory + { + private static readonly MethodInfo _genericCreate + = typeof(ManyToManyLoaderFactory).GetTypeInfo().GetDeclaredMethod(nameof(CreateManyToMany)); + + /// + /// 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. + /// + public virtual ICollectionLoader Create([NotNull] ISkipNavigation skipNavigation) + => (ICollectionLoader)_genericCreate.MakeGenericMethod( + skipNavigation.TargetEntityType.ClrType, + skipNavigation.DeclaringEntityType.ClrType) + .Invoke(null, new object[] { skipNavigation }); + + [UsedImplicitly] + private static ICollectionLoader CreateManyToMany(ISkipNavigation skipNavigation) + where TEntity : class + where TTargetEntity : class + => new ManyToManyLoader(skipNavigation); + } +} diff --git a/src/EFCore/Metadata/Internal/NavigationExtensions.cs b/src/EFCore/Metadata/Internal/NavigationExtensions.cs index 749b532d172..36f38058eae 100644 --- a/src/EFCore/Metadata/Internal/NavigationExtensions.cs +++ b/src/EFCore/Metadata/Internal/NavigationExtensions.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Internal; namespace Microsoft.EntityFrameworkCore.Metadata.Internal { @@ -33,5 +34,14 @@ public static MemberIdentity CreateMemberIdentity([CanBeNull] this INavigation n /// public static Navigation AsNavigation([NotNull] this INavigation navigation, [NotNull] [CallerMemberName] string methodName = "") => MetadataExtensions.AsConcreteMetadataType(navigation, methodName); + + /// + /// 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. + /// + public static ICollectionLoader GetManyToManyLoader([NotNull] this ISkipNavigation navigation) + => ((SkipNavigation)navigation).ManyToManyLoader; } } diff --git a/src/EFCore/Metadata/Internal/SkipNavigation.cs b/src/EFCore/Metadata/Internal/SkipNavigation.cs index 426c188d425..2f968cc46d4 100644 --- a/src/EFCore/Metadata/Internal/SkipNavigation.cs +++ b/src/EFCore/Metadata/Internal/SkipNavigation.cs @@ -27,6 +27,7 @@ public class SkipNavigation : PropertyBase, IMutableSkipNavigation, IConventionS // Warning: Never access these fields directly as access needs to be thread-safe private IClrCollectionAccessor _collectionAccessor; + private ICollectionLoader _manyToManyLoader; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -316,6 +317,16 @@ public virtual IClrCollectionAccessor CollectionAccessor => NonCapturingLazyInitializer.EnsureInitialized( ref _collectionAccessor, this, n => new ClrCollectionAccessorFactory().Create(n)); + /// + /// 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. + /// + public virtual ICollectionLoader ManyToManyLoader + => NonCapturingLazyInitializer.EnsureInitialized( + ref _manyToManyLoader, this, n => new ManyToManyLoaderFactory().Create(this)); + /// /// 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 diff --git a/test/EFCore.InMemory.FunctionalTests/ManyToManyLoadInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/ManyToManyLoadInMemoryTest.cs new file mode 100644 index 00000000000..a4163e47389 --- /dev/null +++ b/test/EFCore.InMemory.FunctionalTests/ManyToManyLoadInMemoryTest.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.TestUtilities; + +namespace Microsoft.EntityFrameworkCore +{ + public class ManyToManyLoadInMemoryTest : ManyToManyLoadTestBase + { + public ManyToManyLoadInMemoryTest(ManyToManyLoadInMemoryFixture fixture) + : base(fixture) + { + } + + public class ManyToManyLoadInMemoryFixture : ManyToManyLoadFixtureBase + { + protected override ITestStoreFactory TestStoreFactory => InMemoryTestStoreFactory.Instance; + } + } +} diff --git a/test/EFCore.Specification.Tests/LoadTestBase.cs b/test/EFCore.Specification.Tests/LoadTestBase.cs index 052b0f635db..b15edfcb0af 100644 --- a/test/EFCore.Specification.Tests/LoadTestBase.cs +++ b/test/EFCore.Specification.Tests/LoadTestBase.cs @@ -1461,55 +1461,28 @@ public virtual void Lazy_loading_uses_field_access_when_abstract_base_class_navi } [ConditionalTheory] - [InlineData(EntityState.Unchanged, true)] - [InlineData(EntityState.Unchanged, false)] - [InlineData(EntityState.Modified, true)] - [InlineData(EntityState.Modified, false)] - [InlineData(EntityState.Deleted, true)] - [InlineData(EntityState.Deleted, false)] - public virtual async Task Load_collection(EntityState state, bool async) + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + public virtual async Task Load_collection(EntityState state, QueryTrackingBehavior queryTrackingBehavior, bool async) { using var context = CreateContext(); - var parent = context.Set().Single(); - - ClearLog(); - - var collectionEntry = context.Entry(parent).Collection(e => e.Children); - - context.Entry(parent).State = state; - - Assert.False(collectionEntry.IsLoaded); - - if (async) - { - await collectionEntry.LoadAsync(); - } - else - { - collectionEntry.Load(); - } - - Assert.True(collectionEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, parent.Children.Count()); - Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); - - Assert.Equal(3, context.ChangeTracker.Entries().Count()); - } - [ConditionalTheory] - [InlineData(EntityState.Unchanged, true)] - [InlineData(EntityState.Unchanged, false)] - [InlineData(EntityState.Modified, true)] - [InlineData(EntityState.Modified, false)] - [InlineData(EntityState.Deleted, true)] - [InlineData(EntityState.Deleted, false)] - public virtual async Task Load_collection_with_NoTracking_behavior(EntityState state, bool async) - { - using var context = CreateContext(); context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; var parent = context.Set().Single(); diff --git a/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs b/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs new file mode 100644 index 00000000000..4ac37d95bc4 --- /dev/null +++ b/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs @@ -0,0 +1,746 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.EntityFrameworkCore.Utilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public abstract class ManyToManyLoadTestBase : IClassFixture + where TFixture : ManyToManyLoadTestBase.ManyToManyLoadFixtureBase + { + protected ManyToManyLoadTestBase(TFixture fixture) => Fixture = fixture; + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + public virtual async Task Load_collection(EntityState state, QueryTrackingBehavior queryTrackingBehavior, bool async) + { + using var context = Fixture.CreateContext(); + + context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + + var left = context.Set().Find(3); + + ClearLog(); + + var collectionEntry = context.Entry(left).Collection(e => e.TwoSkip); + + context.Entry(left).State = state; + + Assert.False(collectionEntry.IsLoaded); + + if (ExpectLazyLoading) + { + Assert.Equal(7, left.TwoSkip.Count); + } + else + { + if (async) + { + await collectionEntry.LoadAsync(); + } + else + { + collectionEntry.Load(); + } + } + + Assert.True(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(7, left.TwoSkip.Count); + foreach (var right in left.TwoSkip) + { + Assert.Contains(left, right.OneSkip); + } + + Assert.Equal(1 + 7 + 7, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Deleted, true)] + [InlineData(EntityState.Deleted, false)] + public virtual async Task Load_collection_using_Query(EntityState state, bool async) + { + using var context = Fixture.CreateContext(); + + var left = context.Set().Find(3); + + ClearLog(); + + var collectionEntry = context.Entry(left).Collection(e => e.TwoSkipShared); + + context.Entry(left).State = state; + + Assert.False(collectionEntry.IsLoaded); + + var children = async + ? await collectionEntry.Query().ToListAsync() + : collectionEntry.Query().ToList(); + + Assert.False(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(3, left.TwoSkipShared.Count); + foreach (var right in left.TwoSkipShared) + { + Assert.Contains(left, right.OneSkipShared); + } + + Assert.Equal(children, left.TwoSkipShared.ToList()); + + Assert.Equal(1 + 3 + 3, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Added, false)] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Added, true)] + public virtual void Attached_collections_are_not_marked_as_loaded(EntityState state, bool lazy) + { + using var context = Fixture.CreateContext(); + + context.ChangeTracker.LazyLoadingEnabled = false; + + var left = ExpectLazyLoading + ? context.CreateProxy( + b => + { + b.Id = 7776; + b.TwoSkip.Add(new EntityTwo { Id = 7777 }); + b.TwoSkipShared.Add(new EntityTwo { Id = 7778 }); + b.SelfSkipPayloadLeft.Add(new EntityOne { Id = 7779 }); + b.SelfSkipPayloadRight.Add(new EntityOne { Id = 7780 }); + b.BranchSkip.Add(new EntityBranch { Id = 7781 }); + b.ThreeSkipPayloadFull.Add(new EntityThree { Id = 7782 }); + b.ThreeSkipPayloadFullShared.Add(new EntityThree { Id = 7783 }); + }) + : new EntityOne + { + Id = 7776, + TwoSkip = { new EntityTwo { Id = 7777 } }, + TwoSkipShared = { new EntityTwo { Id = 7778 } }, + SelfSkipPayloadLeft = { new EntityOne { Id = 7779 } }, + SelfSkipPayloadRight = { new EntityOne { Id = 7780 } }, + BranchSkip = { new EntityBranch { Id = 7781 } }, + ThreeSkipPayloadFull = { new EntityThree { Id = 7782 } }, + ThreeSkipPayloadFullShared = { new EntityThree { Id = 7783 } }, + }; + + context.Attach(left); + + if (state != EntityState.Unchanged) + { + foreach (var child in left.TwoSkip.Cast() + .Concat(left.TwoSkipShared) + .Concat(left.SelfSkipPayloadLeft) + .Concat(left.SelfSkipPayloadRight) + .Concat(left.BranchSkip) + .Concat(left.ThreeSkipPayloadFull) + .Concat(left.TwoSkipShared) + .Concat(left.ThreeSkipPayloadFullShared)) + { + context.Entry(child).State = state; + } + + context.Entry(left).State = state; + + } + + context.ChangeTracker.LazyLoadingEnabled = true; + + Assert.False(context.Entry(left).Collection(e => e.TwoSkip).IsLoaded); + Assert.False(context.Entry(left).Collection(e => e.TwoSkipShared).IsLoaded); + Assert.False(context.Entry(left).Collection(e => e.SelfSkipPayloadLeft).IsLoaded); + Assert.False(context.Entry(left).Collection(e => e.SelfSkipPayloadRight).IsLoaded); + Assert.False(context.Entry(left).Collection(e => e.BranchSkip).IsLoaded); + Assert.False(context.Entry(left).Collection(e => e.ThreeSkipPayloadFull).IsLoaded); + Assert.False(context.Entry(left).Collection(e => e.ThreeSkipPayloadFullShared).IsLoaded); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Deleted, true)] + [InlineData(EntityState.Deleted, false)] + public virtual async Task Load_collection_already_loaded(EntityState state, bool async) + { + using var context = Fixture.CreateContext(); + + var left = context.Set().Include(e => e.ThreeSkipPayloadFull).Single(e => e.Id == 3); + + ClearLog(); + + var collectionEntry = context.Entry(left).Collection(e => e.ThreeSkipPayloadFull); + + context.Entry(left).State = state; + + Assert.True(collectionEntry.IsLoaded); + + if (ExpectLazyLoading) + { + Assert.Equal(4, left.ThreeSkipPayloadFull.Count); + } + else + { + if (async) + { + await collectionEntry.LoadAsync(); + } + else + { + collectionEntry.Load(); + } + } + + Assert.True(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(4, left.ThreeSkipPayloadFull.Count); + foreach (var right in left.ThreeSkipPayloadFull) + { + Assert.Contains(left, right.OneSkipPayloadFull); + } + + Assert.Equal(1 + 4 + 4, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Deleted, true)] + [InlineData(EntityState.Deleted, false)] + public virtual async Task Load_collection_using_Query_already_loaded(EntityState state, bool async) + { + using var context = Fixture.CreateContext(); + + var left = context.Set().Include(e => e.TwoSkip).Single(e => e.Id == 3); + + ClearLog(); + + var collectionEntry = context.Entry(left).Collection(e => e.TwoSkip); + + context.Entry(left).State = state; + + Assert.True(collectionEntry.IsLoaded); + + var children = async + ? await collectionEntry.Query().ToListAsync() + : collectionEntry.Query().ToList(); + + Assert.True(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(7, left.TwoSkip.Count); + foreach (var right in left.TwoSkip) + { + Assert.Contains(left, right.OneSkip); + } + + Assert.Equal(children, left.TwoSkip.ToList()); + + Assert.Equal(1 + 7 + 7, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Deleted, true)] + [InlineData(EntityState.Deleted, false)] + public virtual async Task Load_collection_untyped(EntityState state, bool async) + { + using var context = Fixture.CreateContext(); + + var left = context.Set().Find(3); + + ClearLog(); + + var navigationEntry = context.Entry(left).Navigation("TwoSkip"); + + context.Entry(left).State = state; + + Assert.False(navigationEntry.IsLoaded); + + if (ExpectLazyLoading) + { + Assert.Equal(7, left.TwoSkip.Count); + } + else + { + if (async) + { + await navigationEntry.LoadAsync(); + } + else + { + navigationEntry.Load(); + } + } + + Assert.True(navigationEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(7, left.TwoSkip.Count); + foreach (var right in left.TwoSkip) + { + Assert.Contains(left, right.OneSkip); + } + + Assert.Equal(1 + 7 + 7, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Deleted, true)] + [InlineData(EntityState.Deleted, false)] + public virtual async Task Load_collection_using_Query_untyped(EntityState state, bool async) + { + using var context = Fixture.CreateContext(); + + var left = context.Set().Find(3); + + ClearLog(); + + var collectionEntry = context.Entry(left).Navigation("TwoSkipShared"); + + context.Entry(left).State = state; + + Assert.False(collectionEntry.IsLoaded); + + var children = async + ? await collectionEntry.Query().ToListAsync() + : collectionEntry.Query().ToList(); + + Assert.False(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(3, left.TwoSkipShared.Count); + foreach (var right in left.TwoSkipShared) + { + Assert.Contains(left, right.OneSkipShared); + } + + Assert.Equal(children, left.TwoSkipShared.ToList()); + + Assert.Equal(1 + 3 + 3, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Deleted, true)] + [InlineData(EntityState.Deleted, false)] + public virtual async Task Load_collection_not_found_untyped(EntityState state, bool async) + { + using var context = Fixture.CreateContext(); + + var left = context.Attach( + ExpectLazyLoading + ? context.CreateProxy(b => b.Id = 999) + : new EntityOne { Id = 999 }).Entity; + + ClearLog(); + + var navigationEntry = context.Entry(left).Navigation("TwoSkip"); + + context.Entry(left).State = state; + + Assert.False(navigationEntry.IsLoaded); + + if (ExpectLazyLoading) + { + Assert.Equal(0, left.TwoSkip.Count); + } + else + { + if (async) + { + await navigationEntry.LoadAsync(); + } + else + { + navigationEntry.Load(); + } + } + + Assert.True(navigationEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Empty(left.TwoSkip); + Assert.Single(context.ChangeTracker.Entries()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Deleted, true)] + [InlineData(EntityState.Deleted, false)] + public virtual async Task Load_collection_using_Query_not_found_untyped(EntityState state, bool async) + { + using var context = Fixture.CreateContext(); + + var left = context.Attach( + ExpectLazyLoading + ? context.CreateProxy(b => b.Id = 999) + : new EntityOne { Id = 999 }).Entity; + + ClearLog(); + + var navigationEntry = context.Entry(left).Navigation("TwoSkip"); + + context.Entry(left).State = state; + + Assert.False(navigationEntry.IsLoaded); + + var children = async + ? await navigationEntry.Query().ToListAsync() + : navigationEntry.Query().ToList(); + + Assert.False(navigationEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Empty(children); + Assert.Empty(left.TwoSkip); + + Assert.Single(context.ChangeTracker.Entries()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Unchanged, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Modified, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Modified, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Deleted, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Deleted, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Unchanged, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Unchanged, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Modified, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Modified, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Deleted, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Deleted, false, CascadeTiming.OnSaveChanges)] + public virtual async Task Load_collection_already_loaded_untyped(EntityState state, bool async, CascadeTiming deleteOrphansTiming) + { + using var context = Fixture.CreateContext(); + + context.ChangeTracker.DeleteOrphansTiming = deleteOrphansTiming; + + var left = context.Set().Include(e => e.ThreeSkipPayloadFull).Single(e => e.Id == 3); + + ClearLog(); + + var navigationEntry = context.Entry(left).Navigation("ThreeSkipPayloadFull"); + + context.Entry(left).State = state; + + Assert.True(navigationEntry.IsLoaded); + + if (ExpectLazyLoading) + { + Assert.Equal(4, left.ThreeSkipPayloadFull.Count); + } + else + { + if (async) + { + await navigationEntry.LoadAsync(); + } + else + { + navigationEntry.Load(); + } + } + + Assert.True(navigationEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(4, left.ThreeSkipPayloadFull.Count); + foreach (var right in left.ThreeSkipPayloadFull) + { + Assert.Contains(left, right.OneSkipPayloadFull); + } + + Assert.Equal(1 + 4 + 4, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Unchanged, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Modified, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Modified, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Deleted, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Deleted, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Unchanged, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Unchanged, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Modified, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Modified, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Deleted, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Deleted, false, CascadeTiming.OnSaveChanges)] + public virtual async Task Load_collection_using_Query_already_loaded_untyped( + EntityState state, bool async, CascadeTiming deleteOrphansTiming) + { + using var context = Fixture.CreateContext(); + + context.ChangeTracker.DeleteOrphansTiming = deleteOrphansTiming; + + var left = context.Set().Include(e => e.TwoSkip).Single(e => e.Id == 3); + + ClearLog(); + + var navigationEntry = context.Entry(left).Navigation("TwoSkip"); + + context.Entry(left).State = state; + + Assert.True(navigationEntry.IsLoaded); + + // Issue #16429 + var children = async + ? await navigationEntry.Query().ToListAsync() + : navigationEntry.Query().ToList(); + + Assert.True(navigationEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(7, left.TwoSkip.Count); + foreach (var right in left.TwoSkip) + { + Assert.Contains(left, right.OneSkip); + } + + Assert.Equal(children, left.TwoSkip.ToList()); + + Assert.Equal(1 + 7 + 7, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Deleted, true)] + [InlineData(EntityState.Deleted, false)] + public virtual async Task Load_collection_composite_key(EntityState state, bool async) + { + using var context = Fixture.CreateContext(); + + var left = context.Set().Find(7, "7_2", new DateTime(2007, 2, 1)); + + ClearLog(); + + var collectionEntry = context.Entry(left).Collection(e => e.ThreeSkipFull); + + context.Entry(left).State = state; + + Assert.False(collectionEntry.IsLoaded); + + if (ExpectLazyLoading) + { + Assert.Equal(2, left.ThreeSkipFull.Count); + } + else + { + if (async) + { + await collectionEntry.LoadAsync(); + } + else + { + collectionEntry.Load(); + } + } + + Assert.True(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, left.ThreeSkipFull.Count); + foreach (var right in left.ThreeSkipFull) + { + Assert.Contains(left, right.CompositeKeySkipFull); + } + + Assert.Equal(1 + 2 + 2, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Deleted, true)] + [InlineData(EntityState.Deleted, false)] + public virtual async Task Load_collection_using_Query_composite_key(EntityState state, bool async) + { + using var context = Fixture.CreateContext(); + + var left = context.Set().Find(7, "7_2", new DateTime(2007, 2, 1)); + + ClearLog(); + + var collectionEntry = context.Entry(left).Collection(e => e.ThreeSkipFull); + + context.Entry(left).State = state; + + Assert.False(collectionEntry.IsLoaded); + + var children = async + ? await collectionEntry.Query().ToListAsync() + : collectionEntry.Query().ToList(); + + Assert.False(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, left.ThreeSkipFull.Count); + foreach (var right in left.ThreeSkipFull) + { + Assert.Contains(left, right.CompositeKeySkipFull); + } + + Assert.Equal(children, left.ThreeSkipFull.ToList()); + + Assert.Equal(1 + 2 + 2, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(true, QueryTrackingBehavior.NoTracking)] + [InlineData(true, QueryTrackingBehavior.TrackAll)] + [InlineData(true, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(false, QueryTrackingBehavior.NoTracking)] + [InlineData(false, QueryTrackingBehavior.TrackAll)] + [InlineData(false, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task Load_collection_for_detached_throws(bool async, QueryTrackingBehavior queryTrackingBehavior) + { + using var context = Fixture.CreateContext(); + + var left = context.Set().AsTracking(queryTrackingBehavior).Single(e => e.Id == 3); + + var collectionEntry = context.Entry(left).Collection(e => e.TwoSkip); + + if (queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + context.Entry(left).State = EntityState.Detached; + } + + Assert.Equal( + CoreStrings.CannotLoadDetached(nameof(left.TwoSkip), nameof(EntityOne)), + (await Assert.ThrowsAsync( + async () => + { + if (async) + { + await collectionEntry.LoadAsync(); + } + else + { + collectionEntry.Load(); + } + })).Message); + } + + [ConditionalTheory] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Query_collection_for_detached_throws(QueryTrackingBehavior queryTrackingBehavior) + { + using var context = Fixture.CreateContext(); + + var left = context.Set().AsTracking(queryTrackingBehavior).Single(e => e.Id == 3); + + var collectionEntry = context.Entry(left).Collection(e => e.TwoSkip); + + if (queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + context.Entry(left).State = EntityState.Detached; + } + + Assert.Equal( + CoreStrings.CannotLoadDetached(nameof(left.TwoSkip), nameof(EntityOne)), + Assert.Throws(() => collectionEntry.Query()).Message); + } + + protected virtual void ClearLog() + { + } + + protected virtual void RecordLog() + { + } + + protected TFixture Fixture { get; } + + protected virtual bool ExpectLazyLoading => false; + + public abstract class ManyToManyLoadFixtureBase : ManyToManyQueryFixtureBase + { + protected override string StoreName { get; } = "ManyToManyLoadTest"; + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/LoadSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/LoadSqlServerTest.cs index fd115b48b52..24e987aad5a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/LoadSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/LoadSqlServerTest.cs @@ -373,9 +373,9 @@ public override void Lazy_load_one_to_one_reference_to_principal_null_FK_composi AssertSql(""); } - public override async Task Load_collection(EntityState state, bool async) + public override async Task Load_collection(EntityState state, QueryTrackingBehavior queryTrackingBehavior, bool async) { - await base.Load_collection(state, async); + await base.Load_collection(state, queryTrackingBehavior, async); AssertSql( @"@__p_0='707' (Nullable = true) diff --git a/test/EFCore.SqlServer.FunctionalTests/ManyToManyLoadSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ManyToManyLoadSqlServerTest.cs new file mode 100644 index 00000000000..5d94343a113 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/ManyToManyLoadSqlServerTest.cs @@ -0,0 +1,121 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public class ManyToManyLoadSqlServerTest : ManyToManyLoadTestBase + { + public ManyToManyLoadSqlServerTest(ManyToManyLoadSqlServerFixture fixture) + : base(fixture) + { + } + + public override async Task Load_collection(EntityState state, QueryTrackingBehavior queryTrackingBehavior, bool async) + { + await base.Load_collection(state, queryTrackingBehavior, async); + + AssertSql( + @"@__p_0='3' + +SELECT [t].[Id], [t].[CollectionInverseId], [t].[Name], [t].[ReferenceInverseId], [e].[Id], [t].[OneId], [t].[TwoId], [t0].[OneId], [t0].[TwoId], [t0].[Id], [t0].[Name] +FROM [EntityOnes] AS [e] +INNER JOIN ( + SELECT [e0].[Id], [e0].[CollectionInverseId], [e0].[Name], [e0].[ReferenceInverseId], [j].[OneId], [j].[TwoId] + FROM [JoinOneToTwo] AS [j] + INNER JOIN [EntityTwos] AS [e0] ON [j].[TwoId] = [e0].[Id] +) AS [t] ON [e].[Id] = [t].[OneId] +LEFT JOIN ( + SELECT [j0].[OneId], [j0].[TwoId], [e1].[Id], [e1].[Name] + FROM [JoinOneToTwo] AS [j0] + INNER JOIN [EntityOnes] AS [e1] ON [j0].[OneId] = [e1].[Id] + WHERE [e1].[Id] = @__p_0 +) AS [t0] ON [t].[Id] = [t0].[TwoId] +WHERE [e].[Id] = @__p_0 +ORDER BY [e].[Id], [t].[OneId], [t].[TwoId], [t].[Id], [t0].[OneId], [t0].[TwoId], [t0].[Id]"); + } + + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); + + protected override void RecordLog() => Sql = Fixture.TestSqlLoggerFactory.Sql; + + private const string FileNewLine = @" +"; + + private void AssertSql(string expected) + { + try + { + Assert.Equal( + expected, + Sql, + ignoreLineEndingDifferences: true); + } + catch + { + var methodCallLine = Environment.StackTrace.Split( + new[] { Environment.NewLine }, + StringSplitOptions.RemoveEmptyEntries)[2].Substring(6); + + var testName = methodCallLine.Substring(0, methodCallLine.IndexOf(')') + 1); + var lineIndex = methodCallLine.LastIndexOf("line", StringComparison.Ordinal); + var lineNumber = lineIndex > 0 ? methodCallLine.Substring(lineIndex) : ""; + + var currentDirectory = Directory.GetCurrentDirectory(); + var logFile = currentDirectory.Substring( + 0, + currentDirectory.LastIndexOf("\\artifacts\\", StringComparison.Ordinal) + 1) + + "QueryBaseline.txt"; + + var testInfo = testName + " : " + lineNumber + FileNewLine; + + var newBaseLine = $@" AssertSql( + {"@\"" + Sql.Replace("\"", "\"\"") + "\""}); + +"; + + var contents = testInfo + newBaseLine + FileNewLine + FileNewLine; + + File.AppendAllText(logFile, contents); + + throw; + } + } + + private string Sql { get; set; } + + public class ManyToManyLoadSqlServerFixture : ManyToManyLoadFixtureBase + { + public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder + .Entity() + .Property(e => e.Payload) + .HasDefaultValueSql("GETUTCDATE()"); + + modelBuilder + .SharedTypeEntity>("JoinOneToThreePayloadFullShared") + .IndexerProperty("Payload") + .HasDefaultValue("Generated"); + + modelBuilder + .Entity() + .Property(e => e.Payload) + .HasDefaultValue("Generated"); + } + } + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/ManyToManyLoadProxySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/ManyToManyLoadProxySqliteTest.cs new file mode 100644 index 00000000000..fcdbc3adfea --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/ManyToManyLoadProxySqliteTest.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.EntityFrameworkCore +{ + public class ManyToManyLoadProxySqliteTest + : ManyToManyLoadSqliteTestBase + { + public ManyToManyLoadProxySqliteTest(ManyToManyLoadProxySqliteFixture fixture) + : base(fixture) + { + } + + protected override bool ExpectLazyLoading => true; + + public class ManyToManyLoadProxySqliteFixture : ManyToManyLoadSqliteFixtureBase + { + protected override string StoreName { get; } = "ManyToManyLoadProxies"; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).UseLazyLoadingProxies(); + + protected override IServiceCollection AddServices(IServiceCollection serviceCollection) + => base.AddServices(serviceCollection.AddEntityFrameworkProxies()); + } + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/ManyToManyLoadSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/ManyToManyLoadSqliteTest.cs new file mode 100644 index 00000000000..9308b6a5557 --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/ManyToManyLoadSqliteTest.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.EntityFrameworkCore +{ + public class ManyToManyLoadSqliteTest + : ManyToManyLoadSqliteTestBase + { + public ManyToManyLoadSqliteTest(ManyToManyLoadSqliteFixture fixture) + : base(fixture) + { + } + + public class ManyToManyLoadSqliteFixture : ManyToManyLoadSqliteFixtureBase + { + } + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/ManyToManyLoadSqliteTestBase.cs b/test/EFCore.Sqlite.FunctionalTests/ManyToManyLoadSqliteTestBase.cs new file mode 100644 index 00000000000..4bc8bb981d7 --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/ManyToManyLoadSqliteTestBase.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel; +using Microsoft.EntityFrameworkCore.TestUtilities; + +namespace Microsoft.EntityFrameworkCore +{ + public abstract class ManyToManyLoadSqliteTestBase : ManyToManyLoadTestBase + where TFixture : ManyToManyLoadSqliteTestBase.ManyToManyLoadSqliteFixtureBase + { + protected ManyToManyLoadSqliteTestBase(TFixture fixture) + : base(fixture) + { + } + + public class ManyToManyLoadSqliteFixtureBase : ManyToManyLoadFixtureBase + { + protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder + .Entity() + .Property(e => e.Payload) + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + modelBuilder + .SharedTypeEntity>("JoinOneToThreePayloadFullShared") + .IndexerProperty("Payload") + .HasDefaultValue("Generated"); + + modelBuilder + .Entity() + .Property(e => e.Payload) + .HasDefaultValue("Generated"); + } + } + } +}