From a7e1427e4994a84efc7c110f551ff58bbb557f73 Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Thu, 5 Oct 2017 10:22:38 -0700 Subject: [PATCH] [2.0.1] Query: Fix for #9892 GroupJoin to parent with no child throws invalid operation exception Issue: When we are trying to lift GroupJoin queries without DefaultIfEmpty, we generate LEFT JOIN which means the inner can be null too. But we still have inner key selector doing property access on inner which could throw null ref. If the inner is using EntityShaper then shaper & all access becomes null safe, but in other cases it fails. Solution: Since inner is projecting out a non-entity, we would always have TypedProjectionShaper, which does not have null safe mechanism. Hence For such queries we need to block lifting into lift join Resolves #9892 --- .../Query/RelationalQueryModelVisitor.cs | 41 +++++++++ .../Query/QueryBugsTest.cs | 92 +++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs index a512b608b12..44cb0a3993b 100644 --- a/src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs @@ -1516,6 +1516,19 @@ var innerShapedQuery return false; } + if (!(AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue9892", out var enabled) && enabled)) + { + if (!IsFlattenableGroupJoinDefaultIfEmpty(groupJoinClause, queryModel, index)) + { + var shaperType = innerShapedQuery?.Arguments.Last().Type; + if (shaperType == null + || !typeof(EntityShaper).IsAssignableFrom(shaperType)) + { + return false; + } + } + } + var joinClause = groupJoinClause.JoinClause; var outerQuerySource = FindPreviousQuerySource(queryModel, index); @@ -1630,6 +1643,34 @@ var newShapedQueryMethod return true; } + private bool IsFlattenableGroupJoinDefaultIfEmpty( + [NotNull] GroupJoinClause groupJoinClause, + QueryModel queryModel, + int index) + { + var additionalFromClause + = queryModel.BodyClauses.ElementAtOrDefault(index + 1) + as AdditionalFromClause; + + var subQueryModel + = (additionalFromClause?.FromExpression as SubQueryExpression) + ?.QueryModel; + + var referencedQuerySource + = subQueryModel?.MainFromClause.FromExpression.TryGetReferencedQuerySource(); + + if (referencedQuerySource != groupJoinClause + || queryModel.CountQuerySourceReferences(groupJoinClause) != 1 + || subQueryModel.BodyClauses.Count != 0 + || subQueryModel.ResultOperators.Count != 1 + || !(subQueryModel.ResultOperators[0] is DefaultIfEmptyResultOperator)) + { + return false; + } + + return true; + } + private bool TryFlattenGroupJoinDefaultIfEmpty( [NotNull] GroupJoinClause groupJoinClause, QueryModel queryModel, diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index 8c655c3424a..ed891a1e716 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -2689,6 +2689,98 @@ public class EntityWithLocalVariableAccessInFilter9825 #endregion + #region Bug9892 + + [Fact] + public virtual void GroupJoin_to_parent_with_no_child_works_9892() + { + using (CreateDatabase9892()) + { + using (var context = new MyContext9892(_options)) + { + var results = ( + from p in context.Parents + join c in ( + from x in context.Children + select new + { + ParentId = x.ParentId, + OtherParent = x.OtherParent.Name + }) + on p.Id equals c.ParentId into child + select new + { + ParentId = p.Id, + ParentName = p.Name, + Children = child.Select(c => c.OtherParent) + }).ToList(); + + Assert.Equal(3, results.Count); + Assert.Single(results.Where(t => !t.Children.Any())); + } + } + } + + private SqlServerTestStore CreateDatabase9892() + => CreateTestStore( + () => new MyContext9892(_options), + context => + { + context.Parents.Add(new Parent9892 { Name = "Parent1" }); + context.Parents.Add(new Parent9892 { Name = "Parent2" }); + context.Parents.Add(new Parent9892 { Name = "Parent3" }); + + context.OtherParents.Add(new OtherParent9892 { Name = "OtherParent1" }); + context.OtherParents.Add(new OtherParent9892 { Name = "OtherParent2" }); + + context.SaveChanges(); + + context.Children.Add(new Child9892 { ParentId = 1, OtherParentId = 1 }); + context.Children.Add(new Child9892 { ParentId = 1, OtherParentId = 2 }); + context.Children.Add(new Child9892 { ParentId = 2, OtherParentId = 1 }); + context.Children.Add(new Child9892 { ParentId = 2, OtherParentId = 2 }); + + context.SaveChanges(); + + ClearLog(); + }); + + public class MyContext9892 : DbContext + { + public MyContext9892(DbContextOptions options) + : base(options) + { + } + + public DbSet Parents { get; set; } + public DbSet Children { get; set; } + public DbSet OtherParents { get; set; } + } + + public class Parent9892 + { + public int Id { get; set; } + public string Name { get; set; } + public List Children { get; set; } + } + + public class OtherParent9892 + { + public int Id { get; set; } + public string Name { get; set; } + } + + public class Child9892 + { + public int Id { get; set; } + public int ParentId { get; set; } + public Parent9892 Parent { get; set; } + public int OtherParentId { get; set; } + public OtherParent9892 OtherParent { get; set; } + } + + #endregion + private DbContextOptions _options; private SqlServerTestStore CreateTestStore(