From b706ce66eb67f10f20426cb496331781c0c9ddb0 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Tue, 1 Dec 2020 15:44:08 -0800 Subject: [PATCH 1/3] Stop marking the inverse navigation as loaded when loading a many-to-many navigation Fixes #23475 This is a targeted patch fix which special cases many-to-many loading. I will file an issue for a more general solution using an appropriate general update to query. --- ...ionBindingRemovingExpressionVisitorBase.cs | 15 +++++-- ....CustomShaperCompilingExpressionVisitor.cs | 21 ++++++---- ...sitor.ShaperProcessingExpressionVisitor.cs | 42 ++++++++++++------- .../EntityFrameworkQueryableExtensions.cs | 25 +++++++++++ src/EFCore/Internal/ManyToManyLoader.cs | 4 +- src/EFCore/Query/IncludeExpression.cs | 27 +++++++++++- ...ingExpressionVisitor.ExpressionVisitors.cs | 2 +- ...nExpandingExpressionVisitor.Expressions.cs | 4 ++ .../NavigationExpandingExpressionVisitor.cs | 26 ++++++++++-- ...yableMethodNormalizingExpressionVisitor.cs | 3 +- .../ManyToManyLoadTestBase.cs | 40 ++++++++++++++++++ 11 files changed, 171 insertions(+), 38 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs index 63d948da8e4..79b86f5eaeb 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs @@ -392,7 +392,8 @@ private void AddInclude( Expression.Constant(navigation), Expression.Constant(inverseNavigation, typeof(INavigation)), Expression.Constant(fixup), - Expression.Constant(initialize, typeof(Action<>).MakeGenericType(includingClrType)))); + Expression.Constant(initialize, typeof(Action<>).MakeGenericType(includingClrType)), + Expression.Constant(includeExpression.SetLoaded))); } private static readonly MethodInfo _includeReferenceMethodInfo @@ -409,7 +410,8 @@ private static void IncludeReference( INavigation navigation, INavigation inverseNavigation, Action fixup, - Action _) + Action _, + bool __) { if (entity == null || !navigation.DeclaringEntityType.IsAssignableFrom(entityType)) @@ -454,7 +456,8 @@ private static void IncludeCollection( INavigation navigation, INavigation inverseNavigation, Action fixup, - Action initialize) + Action initialize, + bool setLoaded) { if (entity == null || !navigation.DeclaringEntityType.IsAssignableFrom(entityType)) @@ -485,9 +488,13 @@ private static void IncludeCollection( } else { + if (setLoaded) + { #pragma warning disable EF1001 // Internal EF Core API usage. - entry.SetIsLoaded(navigation); + entry.SetIsLoaded(navigation); #pragma warning restore EF1001 // Internal EF Core API usage. + } + if (relatedEntities != null) { using var enumerator = relatedEntities.GetEnumerator(); diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs index 1bab63556f8..dc114f625c3 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs @@ -87,7 +87,8 @@ private static void IncludeCollection fixup, - bool trackingQuery) + bool trackingQuery, + bool setLoaded) where TIncludingEntity : class, TEntity where TEntity : class where TIncludedEntity : class @@ -97,13 +98,16 @@ private static void IncludeCollection( Func outerIdentifier, INavigationBase navigation, IClrCollectionAccessor clrCollectionAccessor, - bool trackingQuery) + bool trackingQuery, + bool setLoaded) where TParent : class where TNavigationEntity : class, TParent { object collection = null; if (entity is TNavigationEntity) { - if (trackingQuery) - { - queryContext.SetNavigationIsLoaded(entity, navigation); - } - else + if (setLoaded) { - navigation.SetIsLoadedWhenNoTracking(entity); + if (trackingQuery) + { + queryContext.SetNavigationIsLoaded(entity, navigation); + } + else + { + navigation.SetIsLoadedWhenNoTracking(entity); + } } collection = clrCollectionAccessor.GetOrCreate(entity, forMaterialization: true); @@ -1361,20 +1367,24 @@ private static void InitializeSplitIncludeCollection Func parentIdentifier, INavigationBase navigation, IClrCollectionAccessor clrCollectionAccessor, - bool trackingQuery) + bool trackingQuery, + bool setLoaded) where TParent : class where TNavigationEntity : class, TParent { object collection = null; if (entity is TNavigationEntity) { - if (trackingQuery) - { - queryContext.SetNavigationIsLoaded(entity, navigation); - } - else + if (setLoaded) { - navigation.SetIsLoadedWhenNoTracking(entity); + if (trackingQuery) + { + queryContext.SetNavigationIsLoaded(entity, navigation); + } + else + { + navigation.SetIsLoadedWhenNoTracking(entity); + } } collection = clrCollectionAccessor.GetOrCreate(entity, forMaterialization: true); diff --git a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs index 70d6e552e44..782e9e08958 100644 --- a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs +++ b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs @@ -2376,6 +2376,15 @@ internal static readonly MethodInfo IncludeMethodInfo && mi.GetParameters().Any( pi => pi.Name == "navigationPropertyPath" && pi.ParameterType != typeof(string))); + internal static readonly MethodInfo NotQuiteIncludeMethodInfo + = typeof(EntityFrameworkQueryableExtensions) + .GetTypeInfo().GetDeclaredMethods(nameof(NotQuiteInclude)) + .Single( + mi => + mi.GetGenericArguments().Count() == 2 + && mi.GetParameters().Any( + pi => pi.Name == "navigationPropertyPath" && pi.ParameterType != typeof(string))); + /// /// Specifies related entities to include in the query results. The navigation property to be included is specified starting with the /// type of entity being queried (). If you wish to include additional types based on the navigation @@ -2443,6 +2452,22 @@ source.Provider is EntityQueryProvider : source); } + // A version of Include that doesn't set the navigation as loaded + internal static IIncludableQueryable NotQuiteInclude( + [NotNull] this IQueryable source, + [NotNull] Expression> navigationPropertyPath) + where TEntity : class + { + return new IncludableQueryable( + source.Provider is EntityQueryProvider + ? source.Provider.CreateQuery( + Expression.Call( + instance: null, + method: NotQuiteIncludeMethodInfo.MakeGenericMethod(typeof(TEntity), typeof(TProperty)), + arguments: new[] { source.Expression, Expression.Quote(navigationPropertyPath) })) + : source); + } + internal static readonly MethodInfo ThenIncludeAfterEnumerableMethodInfo = typeof(EntityFrameworkQueryableExtensions) .GetTypeInfo().GetDeclaredMethods(nameof(ThenInclude)) diff --git a/src/EFCore/Internal/ManyToManyLoader.cs b/src/EFCore/Internal/ManyToManyLoader.cs index f09dd716729..5138b4be8b9 100644 --- a/src/EFCore/Internal/ManyToManyLoader.cs +++ b/src/EFCore/Internal/ManyToManyLoader.cs @@ -147,7 +147,7 @@ private IQueryable Query( // .AsTracking() // .Where(e => e.Id == left.Id) // .SelectMany(e => e.TwoSkip) - // .Include(e => e.OneSkip.Where(e => e.Id == left.Id)); + // .NotQuiteInclude(e => e.OneSkip.Where(e => e.Id == left.Id)); var queryRoot = _skipNavigation.DeclaringEntityType.HasSharedClrType ? context.Set(_skipNavigation.DeclaringEntityType.Name) @@ -157,7 +157,7 @@ private IQueryable Query( .AsTracking() .Where(BuildWhereLambda(loadProperties, new ValueBuffer(keyValues))) .SelectMany(BuildSelectManyLambda(_skipNavigation)) - .Include(BuildIncludeLambda(_skipNavigation.Inverse, loadProperties, new ValueBuffer(keyValues))) + .NotQuiteInclude(BuildIncludeLambda(_skipNavigation.Inverse, loadProperties, new ValueBuffer(keyValues))) .AsQueryable(); } diff --git a/src/EFCore/Query/IncludeExpression.cs b/src/EFCore/Query/IncludeExpression.cs index 3459faa4db3..fbade51d9d9 100644 --- a/src/EFCore/Query/IncludeExpression.cs +++ b/src/EFCore/Query/IncludeExpression.cs @@ -21,7 +21,8 @@ namespace Microsoft.EntityFrameworkCore.Query public class IncludeExpression : Expression, IPrintableExpression { /// - /// Creates a new instance of the class. + /// Creates a new instance of the class. The navigation will be set + /// as loaded after completing the Include. /// /// An expression to get entity which is performing include. /// An expression to get included navigation element. @@ -30,6 +31,22 @@ public IncludeExpression( [NotNull] Expression entityExpression, [NotNull] Expression navigationExpression, [NotNull] INavigationBase navigation) + : this(entityExpression, navigationExpression, navigation, setLoaded: true) + { + } + + /// + /// Creates a new instance of the class. + /// + /// An expression to get entity which is performing include. + /// An expression to get included navigation element. + /// The navigation for this include operation. + /// True if the navigation will be marked as loaded. + public IncludeExpression( + [NotNull] Expression entityExpression, + [NotNull] Expression navigationExpression, + [NotNull] INavigationBase navigation, + bool setLoaded) { Check.NotNull(entityExpression, nameof(entityExpression)); Check.NotNull(navigationExpression, nameof(navigationExpression)); @@ -39,6 +56,7 @@ public IncludeExpression( NavigationExpression = navigationExpression; Navigation = navigation; Type = EntityExpression.Type; + SetLoaded = setLoaded; } /// @@ -56,6 +74,11 @@ public IncludeExpression( /// public virtual INavigationBase Navigation { get; } + /// + /// True if the navigation will be marked as loaded. + /// + public virtual bool SetLoaded { get; } + /// public sealed override ExpressionType NodeType => ExpressionType.Extension; @@ -87,7 +110,7 @@ public virtual IncludeExpression Update([NotNull] Expression entityExpression, [ Check.NotNull(navigationExpression, nameof(navigationExpression)); return entityExpression != EntityExpression || navigationExpression != NavigationExpression - ? new IncludeExpression(entityExpression, navigationExpression, Navigation) + ? new IncludeExpression(entityExpression, navigationExpression, Navigation, SetLoaded) : this; } diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs index 94cf9a181ac..7bad316b5ed 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs @@ -825,7 +825,7 @@ private Expression ExpandIncludesHelper(Expression root, EntityReference entityR } } - result = new IncludeExpression(result, included, navigationBase); + result = new IncludeExpression(result, included, navigationBase, entityReference.SetLoaded); } return result; diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs index f9afd57ca99..e7c14c3790d 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs @@ -29,6 +29,7 @@ public EntityReference(IEntityType entityType) public bool IsOptional { get; private set; } public IncludeTreeNode IncludePaths { get; private set; } public IncludeTreeNode LastIncludeTreeNode { get; private set; } + public bool SetLoaded { get; private set; } = true; public override ExpressionType NodeType => ExpressionType.Extension; @@ -57,6 +58,9 @@ public void SetLastInclude(IncludeTreeNode lastIncludeTree) public void MarkAsOptional() => IsOptional = true; + public void SuppressSettingLoaded() + => SetLoaded = false; + void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) { Check.NotNull(expressionPrinter, nameof(expressionPrinter)); diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index b5d7b6f5e60..38138ca7767 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -453,13 +453,25 @@ when QueryableMethods.IsSumWithSelector(method): methodCallExpression.Type.TryGetSequenceType()); case nameof(EntityFrameworkQueryableExtensions.Include): + return ProcessInclude( + source, + methodCallExpression.Arguments[1], + thenInclude: false, + setLoaded: true); + case nameof(EntityFrameworkQueryableExtensions.ThenInclude): return ProcessInclude( source, methodCallExpression.Arguments[1], - string.Equals( - method.Name, - nameof(EntityFrameworkQueryableExtensions.ThenInclude))); + thenInclude: true, + setLoaded: true); + + case nameof(EntityFrameworkQueryableExtensions.NotQuiteInclude): + return ProcessInclude( + source, + methodCallExpression.Arguments[1], + thenInclude: false, + setLoaded: false); case nameof(Queryable.GroupBy) when genericMethod == QueryableMethods.GroupByWithKeySelector: @@ -823,7 +835,8 @@ private NavigationExpansionExpression ProcessGroupBy( return new NavigationExpansionExpression(result, navigationTree, navigationTree, parameterName); } - private NavigationExpansionExpression ProcessInclude(NavigationExpansionExpression source, Expression expression, bool thenInclude) + private NavigationExpansionExpression ProcessInclude( + NavigationExpansionExpression source, Expression expression, bool thenInclude, bool setLoaded) { if (source.PendingSelector is NavigationTreeExpression navigationTree && navigationTree.Value is EntityReference entityReference) @@ -890,6 +903,11 @@ private NavigationExpansionExpression ProcessInclude(NavigationExpansionExpressi } entityReference.SetLastInclude(lastIncludeTree); + + if (!setLoaded) + { + entityReference.SuppressSettingLoaded(); + } } return source; diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index 713dcc4a923..022b4d0b8e4 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -80,7 +80,8 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp && method.GetGenericMethodDefinition() is MethodInfo genericMethod && (genericMethod == EntityFrameworkQueryableExtensions.IncludeMethodInfo || genericMethod == EntityFrameworkQueryableExtensions.ThenIncludeAfterEnumerableMethodInfo - || genericMethod == EntityFrameworkQueryableExtensions.ThenIncludeAfterReferenceMethodInfo)) + || genericMethod == EntityFrameworkQueryableExtensions.ThenIncludeAfterReferenceMethodInfo + || genericMethod == EntityFrameworkQueryableExtensions.NotQuiteIncludeMethodInfo)) { var includeLambda = methodCallExpression.Arguments[1].UnwrapLambdaFromQuote(); if (includeLambda.ReturnType.IsGenericType diff --git a/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs b/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs index 17b1554f703..f195d3086eb 100644 --- a/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs +++ b/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs @@ -74,6 +74,10 @@ public virtual async Task Load_collection(EntityState state, QueryTrackingBehavi } Assert.True(collectionEntry.IsLoaded); + foreach (var entityTwo in left.TwoSkip) + { + Assert.False(context.Entry(entityTwo).Collection(e => e.OneSkip).IsLoaded); + } RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; @@ -113,6 +117,10 @@ public virtual async Task Load_collection_using_Query(EntityState state, bool as : collectionEntry.Query().ToList(); Assert.False(collectionEntry.IsLoaded); + foreach (var entityTwo in left.TwoSkipShared) + { + Assert.False(context.Entry(entityTwo).Collection(e => e.OneSkipShared).IsLoaded); + } RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; @@ -234,6 +242,10 @@ public virtual async Task Load_collection_already_loaded(EntityState state, bool } Assert.True(collectionEntry.IsLoaded); + foreach (var entityTwo in left.ThreeSkipPayloadFull) + { + Assert.False(context.Entry(entityTwo).Collection(e => e.OneSkipPayloadFull).IsLoaded); + } RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; @@ -276,6 +288,10 @@ public virtual async Task Load_collection_using_Query_already_loaded(EntityState RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; + foreach (var entityTwo in left.TwoSkip) + { + Assert.False(context.Entry(entityTwo).Collection(e => e.OneSkip).IsLoaded); + } Assert.Equal(7, left.TwoSkip.Count); foreach (var right in left.TwoSkip) @@ -326,6 +342,10 @@ public virtual async Task Load_collection_untyped(EntityState state, bool async) } Assert.True(navigationEntry.IsLoaded); + foreach (var entityTwo in left.TwoSkip) + { + Assert.False(context.Entry((object)entityTwo).Collection("OneSkip").IsLoaded); + } RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; @@ -365,6 +385,10 @@ public virtual async Task Load_collection_using_Query_untyped(EntityState state, : collectionEntry.Query().ToList(); Assert.False(collectionEntry.IsLoaded); + foreach (var entityTwo in left.TwoSkipShared) + { + Assert.False(context.Entry((object)entityTwo).Collection("OneSkipShared").IsLoaded); + } RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; @@ -514,6 +538,10 @@ public virtual async Task Load_collection_already_loaded_untyped(EntityState sta } Assert.True(navigationEntry.IsLoaded); + foreach (var entityTwo in left.ThreeSkipPayloadFull) + { + Assert.False(context.Entry((object)entityTwo).Collection("OneSkipPayloadFull").IsLoaded); + } RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; @@ -565,6 +593,10 @@ public virtual async Task Load_collection_using_Query_already_loaded_untyped( : navigationEntry.Query().ToList(); Assert.True(navigationEntry.IsLoaded); + foreach (var entityTwo in left.TwoSkip) + { + Assert.False(context.Entry((object)entityTwo).Collection("OneSkip").IsLoaded); + } RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; @@ -618,6 +650,10 @@ public virtual async Task Load_collection_composite_key(EntityState state, bool } Assert.True(collectionEntry.IsLoaded); + foreach (var entityTwo in left.ThreeSkipFull) + { + Assert.False(context.Entry(entityTwo).Collection(e => e.CompositeKeySkipFull).IsLoaded); + } RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; @@ -657,6 +693,10 @@ public virtual async Task Load_collection_using_Query_composite_key(EntityState : collectionEntry.Query().ToList(); Assert.False(collectionEntry.IsLoaded); + foreach (var entityTwo in left.ThreeSkipFull) + { + Assert.False(context.Entry(entityTwo).Collection(e => e.CompositeKeySkipFull).IsLoaded); + } RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; From 0100f2d9b340855ed05427df5f9e7671bb2e17a0 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Fri, 4 Dec 2020 14:33:03 -0800 Subject: [PATCH 2/3] Add quirk --- src/EFCore/Internal/ManyToManyLoader.cs | 16 ++++++++++++---- src/EFCore/Query/IncludeExpression.cs | 4 +++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/EFCore/Internal/ManyToManyLoader.cs b/src/EFCore/Internal/ManyToManyLoader.cs index 5138b4be8b9..18056602b86 100644 --- a/src/EFCore/Internal/ManyToManyLoader.cs +++ b/src/EFCore/Internal/ManyToManyLoader.cs @@ -153,12 +153,20 @@ private IQueryable Query( ? context.Set(_skipNavigation.DeclaringEntityType.Name) : context.Set(); - return queryRoot + var queryable = queryRoot .AsTracking() .Where(BuildWhereLambda(loadProperties, new ValueBuffer(keyValues))) - .SelectMany(BuildSelectManyLambda(_skipNavigation)) - .NotQuiteInclude(BuildIncludeLambda(_skipNavigation.Inverse, loadProperties, new ValueBuffer(keyValues))) - .AsQueryable(); + .SelectMany(BuildSelectManyLambda(_skipNavigation)); + + var useOldBehavior = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue23475", out var enabled) && enabled; + + return useOldBehavior + ? queryable + .Include(BuildIncludeLambda(_skipNavigation.Inverse, loadProperties, new ValueBuffer(keyValues))) + .AsQueryable() + : queryable + .NotQuiteInclude(BuildIncludeLambda(_skipNavigation.Inverse, loadProperties, new ValueBuffer(keyValues))) + .AsQueryable(); } private static Expression>> BuildIncludeLambda( diff --git a/src/EFCore/Query/IncludeExpression.cs b/src/EFCore/Query/IncludeExpression.cs index fbade51d9d9..9a94f6cb7f4 100644 --- a/src/EFCore/Query/IncludeExpression.cs +++ b/src/EFCore/Query/IncludeExpression.cs @@ -56,7 +56,9 @@ public IncludeExpression( NavigationExpression = navigationExpression; Navigation = navigation; Type = EntityExpression.Type; - SetLoaded = setLoaded; + + var useOldBehavior = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue23475", out var enabled) && enabled; + SetLoaded = useOldBehavior || setLoaded; } /// From 45d27f2c3cb617d65164335bcd3f948d8309e7a0 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Sat, 5 Dec 2020 16:32:29 -0800 Subject: [PATCH 3/3] Updated to handle case where same navigation is included twice, the first time with setLoaded = false. --- ...ionBindingRemovingExpressionVisitorBase.cs | 2 + ....CustomShaperCompilingExpressionVisitor.cs | 2 + ...sitor.ShaperProcessingExpressionVisitor.cs | 4 + src/EFCore/Query/IncludeExpression.cs | 17 +- ...ingExpressionVisitor.ExpressionVisitors.cs | 4 +- ...nExpandingExpressionVisitor.Expressions.cs | 25 +- .../NavigationExpandingExpressionVisitor.cs | 19 +- .../ManyToManyLoadTestBase.cs | 311 ++++++++++++++++++ .../ManyToManyLoadSqlServerTest.cs | 171 ++++++++++ 9 files changed, 525 insertions(+), 30 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs index 79b86f5eaeb..bb1404170b9 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs @@ -393,7 +393,9 @@ private void AddInclude( Expression.Constant(inverseNavigation, typeof(INavigation)), Expression.Constant(fixup), Expression.Constant(initialize, typeof(Action<>).MakeGenericType(includingClrType)), +#pragma warning disable EF1001 // Internal EF Core API usage. Expression.Constant(includeExpression.SetLoaded))); +#pragma warning restore EF1001 // Internal EF Core API usage. } private static readonly MethodInfo _includeReferenceMethodInfo diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs index dc114f625c3..ec8fb29764a 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs @@ -183,7 +183,9 @@ protected override Expression VisitExtension(Expression extensionExpression) GenerateFixup( includingClrType, relatedEntityClrType, includeExpression.Navigation, inverseNavigation).Compile()), Expression.Constant(_tracking), +#pragma warning disable EF1001 // Internal EF Core API usage. Expression.Constant(includeExpression.SetLoaded)); +#pragma warning restore EF1001 // Internal EF Core API usage. } return Expression.Call( diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs index e27ee90365c..ab21ec8b8ca 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs @@ -548,7 +548,9 @@ protected override Expression VisitExtension(Expression extensionExpression) Expression.Constant(navigation), Expression.Constant(navigation.GetCollectionAccessor()), Expression.Constant(_isTracking), +#pragma warning disable EF1001 // Internal EF Core API usage. Expression.Constant(includeExpression.SetLoaded))); +#pragma warning restore EF1001 // Internal EF Core API usage. var relatedEntityType = innerShaper.ReturnType; var inverseNavigation = navigation.Inverse; @@ -631,7 +633,9 @@ protected override Expression VisitExtension(Expression extensionExpression) Expression.Constant(navigation), Expression.Constant(navigation.GetCollectionAccessor()), Expression.Constant(_isTracking), +#pragma warning disable EF1001 // Internal EF Core API usage. Expression.Constant(includeExpression.SetLoaded))); +#pragma warning restore EF1001 // Internal EF Core API usage. var relatedEntityType = innerShaper.ReturnType; var inverseNavigation = navigation.Inverse; diff --git a/src/EFCore/Query/IncludeExpression.cs b/src/EFCore/Query/IncludeExpression.cs index 9a94f6cb7f4..049b35bdf78 100644 --- a/src/EFCore/Query/IncludeExpression.cs +++ b/src/EFCore/Query/IncludeExpression.cs @@ -4,6 +4,7 @@ using System; using System.Linq.Expressions; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Utilities; @@ -36,12 +37,12 @@ public IncludeExpression( } /// - /// Creates a new instance of the 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. /// - /// An expression to get entity which is performing include. - /// An expression to get included navigation element. - /// The navigation for this include operation. - /// True if the navigation will be marked as loaded. + [EntityFrameworkInternal] public IncludeExpression( [NotNull] Expression entityExpression, [NotNull] Expression navigationExpression, @@ -77,8 +78,12 @@ public IncludeExpression( public virtual INavigationBase Navigation { get; } /// - /// True if the navigation will be marked as loaded. + /// 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. /// + [EntityFrameworkInternal] public virtual bool SetLoaded { get; } /// diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs index 7bad316b5ed..65aefc9b922 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs @@ -825,7 +825,9 @@ private Expression ExpandIncludesHelper(Expression root, EntityReference entityR } } - result = new IncludeExpression(result, included, navigationBase, entityReference.SetLoaded); +#pragma warning disable EF1001 // Internal EF Core API usage. + result = new IncludeExpression(result, included, navigationBase, kvp.Value.SetLoaded); +#pragma warning restore EF1001 // Internal EF Core API usage. } return result; diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs index e7c14c3790d..e23417f7eab 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs @@ -18,7 +18,7 @@ private sealed class EntityReference : Expression, IPrintableExpression public EntityReference(IEntityType entityType) { EntityType = entityType; - IncludePaths = new IncludeTreeNode(entityType, this); + IncludePaths = new IncludeTreeNode(entityType, this, setLoaded: true); } public IEntityType EntityType { get; } @@ -29,7 +29,6 @@ public EntityReference(IEntityType entityType) public bool IsOptional { get; private set; } public IncludeTreeNode IncludePaths { get; private set; } public IncludeTreeNode LastIncludeTreeNode { get; private set; } - public bool SetLoaded { get; private set; } = true; public override ExpressionType NodeType => ExpressionType.Extension; @@ -58,9 +57,6 @@ public void SetLastInclude(IncludeTreeNode lastIncludeTree) public void MarkAsOptional() => IsOptional = true; - public void SuppressSettingLoaded() - => SetLoaded = false; - void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) { Check.NotNull(expressionPrinter, nameof(expressionPrinter)); @@ -90,23 +86,30 @@ private sealed class IncludeTreeNode : Dictionary GetOutgoingEagerLoadedNavigations(IE .Concat(entityType.GetDerivedSkipNavigations()) .Where(n => n.IsEagerLoaded); - private IncludeTreeNode PopulateIncludeTree(IncludeTreeNode includeTreeNode, Expression expression) + private IncludeTreeNode PopulateIncludeTree(IncludeTreeNode includeTreeNode, Expression expression, bool setLoaded) { switch (expression) { @@ -1803,7 +1798,7 @@ private IncludeTreeNode PopulateIncludeTree(IncludeTreeNode includeTreeNode, Exp case MemberExpression memberExpression: var innerExpression = memberExpression.Expression.UnwrapTypeConversion(out var convertedType); - var innerIncludeTreeNode = PopulateIncludeTree(includeTreeNode, innerExpression); + var innerIncludeTreeNode = PopulateIncludeTree(includeTreeNode, innerExpression, setLoaded); var entityType = innerIncludeTreeNode.EntityType; if (convertedType != null) { @@ -1819,7 +1814,7 @@ private IncludeTreeNode PopulateIncludeTree(IncludeTreeNode includeTreeNode, Exp var navigation = entityType.FindNavigation(memberExpression.Member); if (navigation != null) { - var addedNode = innerIncludeTreeNode.AddNavigation(navigation); + var addedNode = innerIncludeTreeNode.AddNavigation(navigation, setLoaded); // This is to add eager Loaded navigations when owner type is included. PopulateEagerLoadedNavigations(addedNode); @@ -1830,7 +1825,7 @@ private IncludeTreeNode PopulateIncludeTree(IncludeTreeNode includeTreeNode, Exp var skipNavigation = entityType.FindSkipNavigation(memberExpression.Member); if (skipNavigation != null) { - var addedNode = innerIncludeTreeNode.AddNavigation(skipNavigation); + var addedNode = innerIncludeTreeNode.AddNavigation(skipNavigation, setLoaded); // This is to add eager Loaded navigations when owner type is included. PopulateEagerLoadedNavigations(addedNode); diff --git a/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs b/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs index f195d3086eb..2202e16d810 100644 --- a/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs +++ b/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs @@ -770,6 +770,317 @@ public virtual void Query_collection_for_detached_throws(QueryTrackingBehavior q Assert.Throws(() => collectionEntry.Query()).Message); } + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Load_collection_using_Query_with_Include(bool async) + { + using var context = Fixture.CreateContext(); + + var left = context.Set().Find(3); + + ClearLog(); + + var collectionEntry = context.Entry(left).Collection(e => e.TwoSkipShared); + + Assert.False(collectionEntry.IsLoaded); + + var children = async + ? await collectionEntry.Query().Include(e => e.ThreeSkipFull).ToListAsync() + : collectionEntry.Query().Include(e => e.ThreeSkipFull).ToList(); + + Assert.False(collectionEntry.IsLoaded); + foreach (var entityTwo in left.TwoSkipShared) + { + Assert.False(context.Entry(entityTwo).Collection(e => e.OneSkipShared).IsLoaded); + Assert.True(context.Entry(entityTwo).Collection(e => e.ThreeSkipFull).IsLoaded); + + foreach (var entityThree in entityTwo.ThreeSkipFull) + { + Assert.False(context.Entry(entityThree).Collection(e => e.TwoSkipFull).IsLoaded); + } + } + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(3, left.TwoSkipShared.Count); + foreach (var right in left.TwoSkipShared) + { + Assert.Contains(left, right.OneSkipShared); + foreach (var three in right.ThreeSkipFull) + { + Assert.Contains(right, three.TwoSkipFull); + } + } + + Assert.Equal(children, left.TwoSkipShared.ToList()); + + Assert.Equal(21, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Load_collection_using_Query_with_Include_for_inverse(bool async) + { + using var context = Fixture.CreateContext(); + + var left = context.Set().Find(3); + + ClearLog(); + + var collectionEntry = context.Entry(left).Collection(e => e.TwoSkipShared); + + Assert.False(collectionEntry.IsLoaded); + + var queryable = collectionEntry.Query().Include(e => e.OneSkipShared); + var children = async + ? await queryable.ToListAsync() + : queryable.ToList(); + + Assert.False(collectionEntry.IsLoaded); + foreach (var entityTwo in left.TwoSkipShared) + { + Assert.True(context.Entry(entityTwo).Collection(e => e.OneSkipShared).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(7, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Load_collection_using_Query_with_Include_for_same_collection(bool async) + { + using var context = Fixture.CreateContext(); + + var left = context.Set().Find(3); + + ClearLog(); + + var collectionEntry = context.Entry(left).Collection(e => e.TwoSkipShared); + + Assert.False(collectionEntry.IsLoaded); + + var queryable = collectionEntry.Query().Include(e => e.OneSkipShared).ThenInclude(e => e.TwoSkipShared); + var children = async + ? await queryable.ToListAsync() + : queryable.ToList(); + + Assert.True(collectionEntry.IsLoaded); + foreach (var entityTwo in left.TwoSkipShared) + { + Assert.True(context.Entry(entityTwo).Collection(e => e.OneSkipShared).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(7, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Load_collection_using_Query_with_filtered_Include(bool async) + { + using var context = Fixture.CreateContext(); + + var left = context.Set().Find(3); + + ClearLog(); + + var collectionEntry = context.Entry(left).Collection(e => e.TwoSkipShared); + + Assert.False(collectionEntry.IsLoaded); + + var children = async + ? await collectionEntry.Query().Include(e => e.ThreeSkipFull.Where(e => e.Id == 13 || e.Id == 11)).ToListAsync() + : collectionEntry.Query().Include(e => e.ThreeSkipFull.Where(e => e.Id == 13 || e.Id == 11)).ToList(); + + Assert.False(collectionEntry.IsLoaded); + foreach (var entityTwo in left.TwoSkipShared) + { + Assert.False(context.Entry(entityTwo).Collection(e => e.OneSkipShared).IsLoaded); + Assert.True(context.Entry(entityTwo).Collection(e => e.ThreeSkipFull).IsLoaded); + + foreach (var entityThree in entityTwo.ThreeSkipFull) + { + Assert.False(context.Entry(entityThree).Collection(e => e.TwoSkipFull).IsLoaded); + } + } + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(3, left.TwoSkipShared.Count); + foreach (var right in left.TwoSkipShared) + { + Assert.Contains(left, right.OneSkipShared); + foreach (var three in right.ThreeSkipFull) + { + Assert.True(three.Id == 11 || three.Id == 13); + Assert.Contains(right, three.TwoSkipFull); + } + } + + Assert.Equal(children, left.TwoSkipShared.ToList()); + + Assert.Equal(9, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Load_collection_using_Query_with_filtered_Include_and_projection(bool async) + { + using var context = Fixture.CreateContext(); + + var left = context.Set().Find(3); + + ClearLog(); + + var collectionEntry = context.Entry(left).Collection(e => e.TwoSkipShared); + + Assert.False(collectionEntry.IsLoaded); + + var queryable = collectionEntry + .Query() + .Include(e => e.ThreeSkipFull.Where(e => e.Id == 13 || e.Id == 11)) + .Select(e => new { e.Id, e.Name, Count1 = e.OneSkipShared.Count, Count3 = e.ThreeSkipFull.Count }); + + var projected = async + ? await queryable.ToListAsync() + : queryable.ToList(); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + Assert.False(collectionEntry.IsLoaded); + Assert.Empty(left.TwoSkipShared); + Assert.Equal(1, context.ChangeTracker.Entries().Count()); + + Assert.Equal(3, projected.Count); + + Assert.Equal(10, projected[0].Id); + Assert.Equal("EntityTwo 10", projected[0].Name); + Assert.Equal(3, projected[0].Count1); + Assert.Equal(1, projected[0].Count3); + + Assert.Equal(11, projected[1].Id); + Assert.Equal("EntityTwo 11", projected[1].Name); + Assert.Equal(2, projected[1].Count1); + Assert.Equal(4, projected[1].Count3); + + Assert.Equal(16, projected[2].Id); + Assert.Equal("EntityTwo 16", projected[2].Name); + Assert.Equal(3, projected[2].Count1); + Assert.Equal(2, projected[2].Count3); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Load_collection_using_Query_with_join(bool async) + { + using var context = Fixture.CreateContext(); + + var left = context.Set().Find(3); + + ClearLog(); + + var collectionEntry = context.Entry(left).Collection(e => e.TwoSkipShared); + + Assert.False(collectionEntry.IsLoaded); + + var queryable = from t in collectionEntry.Query() + join s in context.Set().SelectMany(e => e.TwoSkipShared) + on t.Id equals s.Id + select new { t, s }; + + var projected = async + ? await queryable.ToListAsync() + : queryable.ToList(); + + Assert.False(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(7, context.ChangeTracker.Entries().Count()); + Assert.Equal(8, projected.Count); + + foreach (var pair in projected) + { + Assert.Same(pair.s, pair.t); + } + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Query_with_Include_marks_only_left_as_loaded(bool async) + { + using var context = Fixture.CreateContext(); + + var queryable = context.EntityOnes.Include(e => e.TwoSkip); + var left = async + ? await queryable.SingleAsync(e => e.Id == 1) + : queryable.Single(e => e.Id == 1); + + Assert.True(context.Entry(left).Collection(e => e.TwoSkip).IsLoaded); + + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(20, left.TwoSkip.Count); + foreach (var right in left.TwoSkip) + { + Assert.False(context.Entry(right).Collection(e => e.OneSkip).IsLoaded); + Assert.Same(left, right.OneSkip.Single()); + } + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Query_with_filtered_Include_marks_only_left_as_loaded(bool async) + { + using var context = Fixture.CreateContext(); + + var queryable = context.EntityOnes.Include(e => e.TwoSkip.Where(e => e.Id == 1 || e.Id == 2)); + var left = async + ? await queryable.SingleAsync(e => e.Id == 1) + : queryable.Single(e => e.Id == 1); + + Assert.True(context.Entry(left).Collection(e => e.TwoSkip).IsLoaded); + + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, left.TwoSkip.Count); + foreach (var right in left.TwoSkip) + { + Assert.False(context.Entry(right).Collection(e => e.OneSkip).IsLoaded); + Assert.Same(left, right.OneSkip.Single()); + } + } + protected virtual void ClearLog() { } diff --git a/test/EFCore.SqlServer.FunctionalTests/ManyToManyLoadSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ManyToManyLoadSqlServerTest.cs index 68a402bfc7e..53b31ab5258 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ManyToManyLoadSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ManyToManyLoadSqlServerTest.cs @@ -42,6 +42,177 @@ FROM [JoinOneToTwo] AS [j0] ORDER BY [e].[Id], [t].[OneId], [t].[TwoId], [t].[Id], [t0].[OneId], [t0].[TwoId], [t0].[Id]"); } + + public override async Task Load_collection_using_Query_with_Include_for_inverse(bool async) + { + await base.Load_collection_using_Query_with_Include_for_inverse(async); + + AssertSql( + @"@__p_0='3' + +SELECT [t].[Id], [t].[CollectionInverseId], [t].[Name], [t].[ReferenceInverseId], [e].[Id], [t].[EntityOneId], [t].[EntityTwoId], [t0].[EntityOneId], [t0].[EntityTwoId], [t0].[Id], [t0].[Name] +FROM [EntityOnes] AS [e] +INNER JOIN ( + SELECT [e1].[Id], [e1].[CollectionInverseId], [e1].[Name], [e1].[ReferenceInverseId], [e0].[EntityOneId], [e0].[EntityTwoId] + FROM [EntityOneEntityTwo] AS [e0] + INNER JOIN [EntityTwos] AS [e1] ON [e0].[EntityTwoId] = [e1].[Id] +) AS [t] ON [e].[Id] = [t].[EntityOneId] +LEFT JOIN ( + SELECT [e2].[EntityOneId], [e2].[EntityTwoId], [e3].[Id], [e3].[Name] + FROM [EntityOneEntityTwo] AS [e2] + INNER JOIN [EntityOnes] AS [e3] ON [e2].[EntityOneId] = [e3].[Id] + WHERE [e3].[Id] = @__p_0 +) AS [t0] ON [t].[Id] = [t0].[EntityTwoId] +WHERE [e].[Id] = @__p_0 +ORDER BY [e].[Id], [t].[EntityOneId], [t].[EntityTwoId], [t].[Id], [t0].[EntityOneId], [t0].[EntityTwoId], [t0].[Id]"); + } + + public override async Task Load_collection_using_Query_with_Include_for_same_collection(bool async) + { + await base.Load_collection_using_Query_with_Include_for_same_collection(async); + + AssertSql( + @"@__p_0='3' + +SELECT [t].[Id], [t].[CollectionInverseId], [t].[Name], [t].[ReferenceInverseId], [e].[Id], [t].[EntityOneId], [t].[EntityTwoId], [t1].[EntityOneId], [t1].[EntityTwoId], [t1].[Id], [t1].[Name], [t1].[EntityOneId0], [t1].[EntityTwoId0], [t1].[Id0], [t1].[CollectionInverseId], [t1].[Name0], [t1].[ReferenceInverseId] +FROM [EntityOnes] AS [e] +INNER JOIN ( + SELECT [e1].[Id], [e1].[CollectionInverseId], [e1].[Name], [e1].[ReferenceInverseId], [e0].[EntityOneId], [e0].[EntityTwoId] + FROM [EntityOneEntityTwo] AS [e0] + INNER JOIN [EntityTwos] AS [e1] ON [e0].[EntityTwoId] = [e1].[Id] +) AS [t] ON [e].[Id] = [t].[EntityOneId] +LEFT JOIN ( + SELECT [e2].[EntityOneId], [e2].[EntityTwoId], [e3].[Id], [e3].[Name], [t0].[EntityOneId] AS [EntityOneId0], [t0].[EntityTwoId] AS [EntityTwoId0], [t0].[Id] AS [Id0], [t0].[CollectionInverseId], [t0].[Name] AS [Name0], [t0].[ReferenceInverseId] + FROM [EntityOneEntityTwo] AS [e2] + INNER JOIN [EntityOnes] AS [e3] ON [e2].[EntityOneId] = [e3].[Id] + LEFT JOIN ( + SELECT [e4].[EntityOneId], [e4].[EntityTwoId], [e5].[Id], [e5].[CollectionInverseId], [e5].[Name], [e5].[ReferenceInverseId] + FROM [EntityOneEntityTwo] AS [e4] + INNER JOIN [EntityTwos] AS [e5] ON [e4].[EntityTwoId] = [e5].[Id] + ) AS [t0] ON [e3].[Id] = [t0].[EntityOneId] + WHERE [e3].[Id] = @__p_0 +) AS [t1] ON [t].[Id] = [t1].[EntityTwoId] +WHERE [e].[Id] = @__p_0 +ORDER BY [e].[Id], [t].[EntityOneId], [t].[EntityTwoId], [t].[Id], [t1].[EntityOneId], [t1].[EntityTwoId], [t1].[Id], [t1].[EntityOneId0], [t1].[EntityTwoId0], [t1].[Id0]"); + } + + public override async Task Load_collection_using_Query_with_Include(bool async) + { + await base.Load_collection_using_Query_with_Include(async); + + AssertSql( + @"@__p_0='3' + +SELECT [t].[Id], [t].[CollectionInverseId], [t].[Name], [t].[ReferenceInverseId], [e].[Id], [t].[EntityOneId], [t].[EntityTwoId], [t0].[EntityOneId], [t0].[EntityTwoId], [t0].[Id], [t0].[Name], [t1].[ThreeId], [t1].[TwoId], [t1].[Id], [t1].[CollectionInverseId], [t1].[Name], [t1].[ReferenceInverseId] +FROM [EntityOnes] AS [e] +INNER JOIN ( + SELECT [e1].[Id], [e1].[CollectionInverseId], [e1].[Name], [e1].[ReferenceInverseId], [e0].[EntityOneId], [e0].[EntityTwoId] + FROM [EntityOneEntityTwo] AS [e0] + INNER JOIN [EntityTwos] AS [e1] ON [e0].[EntityTwoId] = [e1].[Id] +) AS [t] ON [e].[Id] = [t].[EntityOneId] +LEFT JOIN ( + SELECT [e2].[EntityOneId], [e2].[EntityTwoId], [e3].[Id], [e3].[Name] + FROM [EntityOneEntityTwo] AS [e2] + INNER JOIN [EntityOnes] AS [e3] ON [e2].[EntityOneId] = [e3].[Id] + WHERE [e3].[Id] = @__p_0 +) AS [t0] ON [t].[Id] = [t0].[EntityTwoId] +LEFT JOIN ( + SELECT [j].[ThreeId], [j].[TwoId], [e4].[Id], [e4].[CollectionInverseId], [e4].[Name], [e4].[ReferenceInverseId] + FROM [JoinTwoToThree] AS [j] + INNER JOIN [EntityThrees] AS [e4] ON [j].[ThreeId] = [e4].[Id] +) AS [t1] ON [t].[Id] = [t1].[TwoId] +WHERE [e].[Id] = @__p_0 +ORDER BY [e].[Id], [t].[EntityOneId], [t].[EntityTwoId], [t].[Id], [t0].[EntityOneId], [t0].[EntityTwoId], [t0].[Id], [t1].[ThreeId], [t1].[TwoId], [t1].[Id]"); + } + + public override async Task Load_collection_using_Query_with_filtered_Include(bool async) + { + await base.Load_collection_using_Query_with_filtered_Include(async); + + AssertSql( + @"@__p_0='3' + +SELECT [t].[Id], [t].[CollectionInverseId], [t].[Name], [t].[ReferenceInverseId], [e].[Id], [t].[EntityOneId], [t].[EntityTwoId], [t0].[EntityOneId], [t0].[EntityTwoId], [t0].[Id], [t0].[Name], [t1].[ThreeId], [t1].[TwoId], [t1].[Id], [t1].[CollectionInverseId], [t1].[Name], [t1].[ReferenceInverseId] +FROM [EntityOnes] AS [e] +INNER JOIN ( + SELECT [e1].[Id], [e1].[CollectionInverseId], [e1].[Name], [e1].[ReferenceInverseId], [e0].[EntityOneId], [e0].[EntityTwoId] + FROM [EntityOneEntityTwo] AS [e0] + INNER JOIN [EntityTwos] AS [e1] ON [e0].[EntityTwoId] = [e1].[Id] +) AS [t] ON [e].[Id] = [t].[EntityOneId] +LEFT JOIN ( + SELECT [e2].[EntityOneId], [e2].[EntityTwoId], [e3].[Id], [e3].[Name] + FROM [EntityOneEntityTwo] AS [e2] + INNER JOIN [EntityOnes] AS [e3] ON [e2].[EntityOneId] = [e3].[Id] + WHERE [e3].[Id] = @__p_0 +) AS [t0] ON [t].[Id] = [t0].[EntityTwoId] +LEFT JOIN ( + SELECT [j].[ThreeId], [j].[TwoId], [e4].[Id], [e4].[CollectionInverseId], [e4].[Name], [e4].[ReferenceInverseId] + FROM [JoinTwoToThree] AS [j] + INNER JOIN [EntityThrees] AS [e4] ON [j].[ThreeId] = [e4].[Id] + WHERE [e4].[Id] IN (13, 11) +) AS [t1] ON [t].[Id] = [t1].[TwoId] +WHERE [e].[Id] = @__p_0 +ORDER BY [e].[Id], [t].[EntityOneId], [t].[EntityTwoId], [t].[Id], [t0].[EntityOneId], [t0].[EntityTwoId], [t0].[Id], [t1].[ThreeId], [t1].[TwoId], [t1].[Id]"); + } + + public override async Task Load_collection_using_Query_with_filtered_Include_and_projection(bool async) + { + await base.Load_collection_using_Query_with_filtered_Include_and_projection(async); + + AssertSql( + @"@__p_0='3' + +SELECT [t].[Id], [t].[Name], ( + SELECT COUNT(*) + FROM [EntityOneEntityTwo] AS [e] + INNER JOIN [EntityOnes] AS [e0] ON [e].[EntityOneId] = [e0].[Id] + WHERE [t].[Id] = [e].[EntityTwoId]) AS [Count1], ( + SELECT COUNT(*) + FROM [JoinTwoToThree] AS [j] + INNER JOIN [EntityThrees] AS [e1] ON [j].[ThreeId] = [e1].[Id] + WHERE [t].[Id] = [j].[TwoId]) AS [Count3] +FROM [EntityOnes] AS [e2] +INNER JOIN ( + SELECT [e4].[Id], [e4].[Name], [e3].[EntityOneId] + FROM [EntityOneEntityTwo] AS [e3] + INNER JOIN [EntityTwos] AS [e4] ON [e3].[EntityTwoId] = [e4].[Id] +) AS [t] ON [e2].[Id] = [t].[EntityOneId] +WHERE [e2].[Id] = @__p_0"); + } + + public override async Task Load_collection_using_Query_with_join(bool async) + { + await base.Load_collection_using_Query_with_join(async); + + AssertSql( + @"@__p_0='3' + +SELECT [t].[Id], [t].[CollectionInverseId], [t].[Name], [t].[ReferenceInverseId], [t1].[Id0], [t1].[CollectionInverseId], [t1].[Name0], [t1].[ReferenceInverseId], [e].[Id], [t].[EntityOneId], [t].[EntityTwoId], [t1].[Id], [t1].[EntityOneId], [t1].[EntityTwoId], [t2].[EntityOneId], [t2].[EntityTwoId], [t2].[Id], [t2].[Name] +FROM [EntityOnes] AS [e] +INNER JOIN ( + SELECT [e1].[Id], [e1].[CollectionInverseId], [e1].[Name], [e1].[ReferenceInverseId], [e0].[EntityOneId], [e0].[EntityTwoId] + FROM [EntityOneEntityTwo] AS [e0] + INNER JOIN [EntityTwos] AS [e1] ON [e0].[EntityTwoId] = [e1].[Id] +) AS [t] ON [e].[Id] = [t].[EntityOneId] +INNER JOIN ( + SELECT [e2].[Id], [t0].[Id] AS [Id0], [t0].[CollectionInverseId], [t0].[Name] AS [Name0], [t0].[ReferenceInverseId], [t0].[EntityOneId], [t0].[EntityTwoId] + FROM [EntityOnes] AS [e2] + INNER JOIN ( + SELECT [e4].[Id], [e4].[CollectionInverseId], [e4].[Name], [e4].[ReferenceInverseId], [e3].[EntityOneId], [e3].[EntityTwoId] + FROM [EntityOneEntityTwo] AS [e3] + INNER JOIN [EntityTwos] AS [e4] ON [e3].[EntityTwoId] = [e4].[Id] + ) AS [t0] ON [e2].[Id] = [t0].[EntityOneId] +) AS [t1] ON [t].[Id] = [t1].[Id0] +LEFT JOIN ( + SELECT [e5].[EntityOneId], [e5].[EntityTwoId], [e6].[Id], [e6].[Name] + FROM [EntityOneEntityTwo] AS [e5] + INNER JOIN [EntityOnes] AS [e6] ON [e5].[EntityOneId] = [e6].[Id] + WHERE [e6].[Id] = @__p_0 +) AS [t2] ON [t].[Id] = [t2].[EntityTwoId] +WHERE [e].[Id] = @__p_0 +ORDER BY [e].[Id], [t].[EntityOneId], [t].[EntityTwoId], [t].[Id], [t1].[Id], [t1].[EntityOneId], [t1].[EntityTwoId], [t1].[Id0], [t2].[EntityOneId], [t2].[EntityTwoId], [t2].[Id]"); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear();