diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs index 4b6c58fffd1..77162079348 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs @@ -478,22 +478,26 @@ MethodInfo GetMethod() return null; } - if (subqueryTranslation.ShaperExpression is EntityShaperExpression entityShaperExpression) + var shaperExpression = subqueryTranslation.ShaperExpression; + var innerExpression = shaperExpression; + Type convertedType = null; + if (shaperExpression is UnaryExpression unaryExpression + && unaryExpression.NodeType == ExpressionType.Convert) { - return new EntityReferenceExpression(subqueryTranslation); + convertedType = unaryExpression.Type; + innerExpression = unaryExpression.Operand; } - var shaperExpression = subqueryTranslation.ShaperExpression; - if (shaperExpression is UnaryExpression unaryExpression - && unaryExpression.NodeType == ExpressionType.Convert - && unaryExpression.Type.MakeNullable() == unaryExpression.Operand.Type) + if (innerExpression is EntityShaperExpression entityShaperExpression + && (convertedType == null + || convertedType.IsAssignableFrom(entityShaperExpression.Type))) { - shaperExpression = unaryExpression.Operand; + return new EntityReferenceExpression(subqueryTranslation.UpdateShaperExpression(innerExpression)); } -#pragma warning disable IDE0046 // Convert to conditional expression - if (!(shaperExpression is ProjectionBindingExpression projectionBindingExpression)) -#pragma warning restore IDE0046 // Convert to conditional expression + if (!(innerExpression is ProjectionBindingExpression projectionBindingExpression + && (convertedType == null + || convertedType.MakeNullable() == innerExpression.Type))) { return null; } @@ -917,9 +921,23 @@ private Expression BindProperty(EntityReferenceExpression entityReferenceExpress if (entityReferenceExpression.SubqueryEntity != null) { var entityShaper = (EntityShaperExpression)entityReferenceExpression.SubqueryEntity.ShaperExpression; - var readValueExpression = ((EntityProjectionExpression)Visit(entityShaper.ValueBufferExpression)).BindProperty(property); var inMemoryQueryExpression = (InMemoryQueryExpression)entityReferenceExpression.SubqueryEntity.QueryExpression; + Expression readValueExpression; + var projectionBindingExpression = (ProjectionBindingExpression)entityShaper.ValueBufferExpression; + if (projectionBindingExpression.ProjectionMember != null) + { + var entityProjectionExpression = (EntityProjectionExpression)inMemoryQueryExpression.GetMappedProjection( + projectionBindingExpression.ProjectionMember); + readValueExpression = entityProjectionExpression.BindProperty(property); + } + else + { + // This has to be index map since entities cannot map to just integer index + var index = projectionBindingExpression.IndexMap[property]; + readValueExpression = inMemoryQueryExpression.Projection[index]; + } + return ProcessSingleResultScalar( inMemoryQueryExpression.ServerQueryExpression, readValueExpression, diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 05eb6b0b20d..c2d21c7b2a6 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -549,16 +549,26 @@ static bool IsAggregateResultWithCustomShaper(MethodInfo method) return null; } - if (subqueryTranslation.ShaperExpression is EntityShaperExpression entityShaperExpression) + var shaperExpression = subqueryTranslation.ShaperExpression; + var innerExpression = shaperExpression; + Type convertedType = null; + if (shaperExpression is UnaryExpression unaryExpression + && unaryExpression.NodeType == ExpressionType.Convert) { - return new EntityReferenceExpression(subqueryTranslation); + convertedType = unaryExpression.Type; + innerExpression = unaryExpression.Operand; } - if (!(subqueryTranslation.ShaperExpression is ProjectionBindingExpression - || (subqueryTranslation.ShaperExpression is UnaryExpression unaryExpression - && unaryExpression.NodeType == ExpressionType.Convert - && unaryExpression.Type.MakeNullable() == unaryExpression.Operand.Type - && unaryExpression.Operand is ProjectionBindingExpression) + if (innerExpression is EntityShaperExpression entityShaperExpression + && (convertedType == null + || convertedType.IsAssignableFrom(entityShaperExpression.Type))) + { + return new EntityReferenceExpression(subqueryTranslation.UpdateShaperExpression(innerExpression)); + } + + if (!((innerExpression is ProjectionBindingExpression + && (convertedType == null + || convertedType.MakeNullable() == innerExpression.Type)) || IsAggregateResultWithCustomShaper(methodCallExpression.Method))) { return null; @@ -583,11 +593,11 @@ static bool IsAggregateResultWithCustomShaper(MethodInfo method) SqlExpression scalarSubqueryExpression = new ScalarSubqueryExpression(subquery); if (subqueryTranslation.ResultCardinality == ResultCardinality.SingleOrDefault - && !subqueryTranslation.ShaperExpression.Type.IsNullableType()) + && !shaperExpression.Type.IsNullableType()) { scalarSubqueryExpression = _sqlExpressionFactory.Coalesce( scalarSubqueryExpression, - (SqlExpression)Visit(subqueryTranslation.ShaperExpression.Type.GetDefaultValueConstant())); + (SqlExpression)Visit(shaperExpression.Type.GetDefaultValueConstant())); } return scalarSubqueryExpression; @@ -942,8 +952,24 @@ private SqlExpression BindProperty(EntityReferenceExpression entityReferenceExpr if (entityReferenceExpression.SubqueryEntity != null) { var entityShaper = (EntityShaperExpression)entityReferenceExpression.SubqueryEntity.ShaperExpression; - var innerProjection = ((EntityProjectionExpression)Visit(entityShaper.ValueBufferExpression)).BindProperty(property); var subSelectExpression = (SelectExpression)entityReferenceExpression.SubqueryEntity.QueryExpression; + + SqlExpression innerProjection; + var projectionBindingExpression = (ProjectionBindingExpression)entityShaper.ValueBufferExpression; + if (projectionBindingExpression.ProjectionMember != null) + { + var entityProjectionExpression = (EntityProjectionExpression)subSelectExpression.GetMappedProjection( + projectionBindingExpression.ProjectionMember); + innerProjection = entityProjectionExpression.BindProperty(property); + } + else + { + // This has to be index map since entities cannot map to just integer index + var index = projectionBindingExpression.IndexMap[property]; + innerProjection = subSelectExpression.Projection[index].Expression; + subSelectExpression.ClearProjection(); + } + subSelectExpression.AddToProjection(innerProjection); return new ScalarSubqueryExpression(subSelectExpression); diff --git a/test/EFCore.InMemory.FunctionalTests/Query/QueryBugsInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/QueryBugsInMemoryTest.cs index 09d2e0da933..792a052ce71 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/QueryBugsInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/QueryBugsInMemoryTest.cs @@ -772,6 +772,119 @@ private class CustomerView19708 #endregion + #region Issue21768 + + [ConditionalFact] + public virtual void Using_explicit_interface_implementation_as_navigation_works() + { + using (CreateScratch((t) => { }, "21768")) + { + using var context = new MyContext21768(); + Expression> projection = b => new BookViewModel21768 + { + FirstPage = b.FrontCover.Illustrations.FirstOrDefault(i => i.State >= IllustrationState21768.Approved) != null + ? new PageViewModel21768 + { + Uri = b.FrontCover.Illustrations.FirstOrDefault(i => i.State >= IllustrationState21768.Approved).Uri + } + : null, + }; + + var result = context.Books.Where(b => b.Id == 1).Select(projection).SingleOrDefault(); + } + } + + private class BookViewModel21768 + { + public PageViewModel21768 FirstPage { get; set; } + } + + private class PageViewModel21768 + { + public string Uri { get; set; } + } + + private interface IBook21768 + { + public int Id { get; set; } + + public IBookCover21768 FrontCover { get; } + public int FrontCoverId { get; set; } + + public IBookCover21768 BackCover { get; } + public int BackCoverId { get; set; } + } + + private interface IBookCover21768 + { + public int Id { get; set; } + public IEnumerable Illustrations { get; } + } + + private interface ICoverIllustration21768 + { + public int Id { get; set; } + public IBookCover21768 Cover { get; } + public int CoverId { get; set; } + public string Uri { get; set; } + public IllustrationState21768 State { get; set; } + } + + private class Book21768 : IBook21768 + { + public int Id { get; set; } + + public BookCover21768 FrontCover { get; set; } + public int FrontCoverId { get; set; } + + public BookCover21768 BackCover { get; set; } + public int BackCoverId { get; set; } + IBookCover21768 IBook21768.FrontCover => FrontCover; + IBookCover21768 IBook21768.BackCover => BackCover; + } + + private class BookCover21768 : IBookCover21768 + { + public int Id { get; set; } + public ICollection Illustrations { get; set; } + IEnumerable IBookCover21768.Illustrations => Illustrations; + } + + private class CoverIllustration21768 : ICoverIllustration21768 + { + public int Id { get; set; } + public BookCover21768 Cover { get; set; } + public int CoverId { get; set; } + public string Uri { get; set; } + public IllustrationState21768 State { get; set; } + + IBookCover21768 ICoverIllustration21768.Cover => Cover; + } + + private enum IllustrationState21768 + { + New, + PendingApproval, + Approved, + Printed + } + + private class MyContext21768 : DbContext + { + public DbSet Books { get; set; } + public DbSet BookCovers { get; set; } + public DbSet CoverIllustrations { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) + .UseInMemoryDatabase("21768"); + } + } + + #endregion + #region SharedHelper private static InMemoryTestStore CreateScratch(Action seed, string databaseName) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index 46fa22271b7..ba6bcb06bff 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -7891,6 +7891,149 @@ private SqlServerTestStore CreateDatabase21666() #endregion + #region Issue21768 + + [ConditionalFact] + public virtual void Using_explicit_interface_implementation_as_navigation_works() + { + using (CreateDatabase21768()) + { + using var context = new MyContext21768(_options); + Expression> projection = b => new BookViewModel21768 + { + FirstPage = b.FrontCover.Illustrations.FirstOrDefault(i => i.State >= IllustrationState21768.Approved) != null + ? new PageViewModel21768 + { + Uri = b.FrontCover.Illustrations.FirstOrDefault(i => i.State >= IllustrationState21768.Approved).Uri + } + : null, + }; + + var result = context.Books.Where(b => b.Id == 1).Select(projection).SingleOrDefault(); + + AssertSql( + @"SELECT TOP(2) CASE + WHEN ( + SELECT TOP(1) [c].[Id] + FROM [CoverIllustrations] AS [c] + WHERE ([b0].[Id] = [c].[CoverId]) AND ([c].[State] >= 2)) IS NOT NULL THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END, ( + SELECT TOP(1) [c0].[Uri] + FROM [CoverIllustrations] AS [c0] + WHERE ([b0].[Id] = [c0].[CoverId]) AND ([c0].[State] >= 2)) +FROM [Books] AS [b] +INNER JOIN [BookCovers] AS [b0] ON [b].[FrontCoverId] = [b0].[Id] +WHERE [b].[Id] = 1"); + } + } + + private class BookViewModel21768 + { + public PageViewModel21768 FirstPage { get; set; } + } + + private class PageViewModel21768 + { + public string Uri { get; set; } + } + + private interface IBook21768 + { + public int Id { get; set; } + + public IBookCover21768 FrontCover { get; } + public int FrontCoverId { get; set; } + + public IBookCover21768 BackCover { get; } + public int BackCoverId { get; set; } + } + + private interface IBookCover21768 + { + public int Id { get; set; } + public IEnumerable Illustrations { get; } + } + + private interface ICoverIllustration21768 + { + public int Id { get; set; } + public IBookCover21768 Cover { get; } + public int CoverId { get; set; } + public string Uri { get; set; } + public IllustrationState21768 State { get; set; } + } + + private class Book21768 : IBook21768 + { + public int Id { get; set; } + + public BookCover21768 FrontCover { get; set; } + public int FrontCoverId { get; set; } + + public BookCover21768 BackCover { get; set; } + public int BackCoverId { get; set; } + IBookCover21768 IBook21768.FrontCover => FrontCover; + IBookCover21768 IBook21768.BackCover => BackCover; + } + + private class BookCover21768 : IBookCover21768 + { + public int Id { get; set; } + public ICollection Illustrations { get; set; } + IEnumerable IBookCover21768.Illustrations => Illustrations; + } + + private class CoverIllustration21768 : ICoverIllustration21768 + { + public int Id { get; set; } + public BookCover21768 Cover { get; set; } + public int CoverId { get; set; } + public string Uri { get; set; } + public IllustrationState21768 State { get; set; } + + IBookCover21768 ICoverIllustration21768.Cover => Cover; + } + + private enum IllustrationState21768 + { + New, + PendingApproval, + Approved, + Printed + } + + private class MyContext21768 : DbContext + { + public DbSet Books { get; set; } + public DbSet BookCovers { get; set; } + public DbSet CoverIllustrations { get; set; } + + public MyContext21768(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + foreach (var fk in modelBuilder.Model.GetEntityTypes().SelectMany(e => e.GetForeignKeys())) + { + fk.DeleteBehavior = DeleteBehavior.NoAction; + } + } + } + + private SqlServerTestStore CreateDatabase21768() + => CreateTestStore( + () => new MyContext21768(_options), + context => + { + context.SaveChanges(); + + ClearLog(); + }); + + #endregion private DbContextOptions _options; private SqlServerTestStore CreateTestStore(