Exception when projecting from navigation properties after null coalescing operator in self-referencing relationship

Open
#20,017 4 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
35/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Stale
Tech stack
csharp
Domain
databases

Research direction

Start with the NullCoalescingTests.ShouldTranslateProjectionOnNullCoalescedEntity reproduction and run it against EF Core 3.1.2 with SQL Server. Trace the failure through RelationalProjectionBindingExpressionVisitor and the stack-traced query translation path. Done means the query reaches ToList successfully and the existing assertions pass without the ParameterExpression exception.

Written by the indexing model from the issue text.

Description

area-query customer-reported

Hi,

To simplify things I will treat the domain model for reproducing the issue as "our domain".

We have an entity Product with a self-referencing optional (!) relationship:

Product WelcomePackage { get; set; }

In a query we project properties from this entity, as well as ones from other related entities - e.g. Documents. For this projections we use some kind of fallback - if WelcomePackage is set, we treat that as the entity to project from, else we fall back to the selected Product.
Basically let projectionProduct = (product.WelcomePackage ?? product)

When we try to project projectionProduct.Documents.Select(...)/FirstOrDefault().Id/... an exception is thrown:
System.InvalidOperationException : When called from 'VisitLambda', rewriting a node of type 'System.Linq.Expressions.ParameterExpression' must return a non-null value of the same type. Alternatively, override 'VisitLambda' and change it to not visit children of this type.
Stack traced included below.

Steps to reproduce
namespace NullCoalescingReproduction
{
    public class NullCoalescingTests : IDisposable
    {
        private readonly Context _context;

        public NullCoalescingTests()
        {
            _context = new Context();
            _context.Database.EnsureDeleted();
            _context.Database.EnsureCreated();
            Seed(_context);
        }

        [Fact]
        public void ShouldTranslateProjectionOnNullCoalescedEntity()
        {
            var query = from product in _context.Set<Product>()
                        //where product.Id == <Some key provided by a user interaction>
                        let welcomePackage = product.WelcomePackage
                        let productToShip = (welcomePackage ?? product)

                        let firstDocument = productToShip.Documents.FirstOrDefault()

                        select new
                        {
                            ProductId = productToShip.Id, //This works
                            EveryDocument = productToShip.Documents, //This works
                            FirstDocument = firstDocument, //This works

                            FirstDocumentId = firstDocument.Id //This does not
                        };

            var result = query.ToList();

            Assert.NotNull(result);
            Assert.NotEmpty(result);
        }

        private static void Seed(Context ctx)
        {
            var welcomePackage = new Product
            {
                Name = "A welcome package with Product A in it"
            };

            var product = new Product
            {
                Name = "Product A",
                WelcomePackage = welcomePackage
            };

            ctx.Set<Product>().AddRange(product, welcomePackage);
            ctx.SaveChanges();
        }

        public void Dispose()
        {
            //I know, that this is not the proper way of implementing IDisposable
            _context.Dispose();
        }
    }

    public class Context : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            var connectionString = "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=ReproduceNullCoalescingBug;Integrated Security=True;";
            optionsBuilder.UseSqlServer(connectionString);
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Product>();
            modelBuilder.Entity<Document>();
        }
    }

    public class Product
    {
        [Key]
        public Guid Id { get; set; }
        public string Name { get; set; }
        public ISet<Document> Documents { get; } = new HashSet<Document>();
        //Consider this being some kind of welcome-package with the ordered product and some additions in it
        public Product WelcomePackage { get; set; }
    }

    public class Document
    {
        [Key]
        public Guid Id { get; set; }

        [Required]
        public Product Product { get; set; }
    }
}

Exception:

 Message: 
    System.InvalidOperationException : When called from 'VisitLambda', rewriting a node of type 'System.Linq.Expressions.ParameterExpression' must return a non-null value of the same type. Alternatively, override 'VisitLambda' and change it to not visit children of this type.
  Stack Trace: 
    ExpressionVisitor.VisitAndConvert[T](T node, String callerName)
    ExpressionVisitorUtils.VisitParameters(ExpressionVisitor visitor, IParameterProvider nodes, String callerName)
    ExpressionVisitor.VisitLambda[T](Expression`1 node)
    Expression`1.Accept(ExpressionVisitor visitor)
    ExpressionVisitor.Visit(Expression node)
    RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
    ExpressionVisitorUtils.VisitArguments(ExpressionVisitor visitor, IArgumentProvider nodes)
    ExpressionVisitor.VisitMethodCall(MethodCallExpression node)
    MethodCallExpression.Accept(ExpressionVisitor visitor)
    ExpressionVisitor.Visit(Expression node)
    RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
    ExpressionVisitorUtils.VisitArguments(ExpressionVisitor visitor, IArgumentProvider nodes)
    ExpressionVisitor.VisitMethodCall(MethodCallExpression node)
    MethodCallExpression.Accept(ExpressionVisitor visitor)
    ExpressionVisitor.Visit(Expression node)
    RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
    ExpressionVisitor.VisitUnary(UnaryExpression node)
    UnaryExpression.Accept(ExpressionVisitor visitor)
    ExpressionVisitor.Visit(Expression node)
    RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
    RelationalProjectionBindingExpressionVisitor.VisitNew(NewExpression newExpression)
    NewExpression.Accept(ExpressionVisitor visitor)
    ExpressionVisitor.Visit(Expression node)
    RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
    RelationalProjectionBindingExpressionVisitor.Translate(SelectExpression selectExpression, Expression expression)
    RelationalQueryableMethodTranslatingExpressionVisitor.TranslateSelect(ShapedQueryExpression source, LambdaExpression selector)
    QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
    RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
    MethodCallExpression.Accept(ExpressionVisitor visitor)
    ExpressionVisitor.Visit(Expression node)
    QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
    Database.CompileQuery[TResult](Expression query, Boolean async)
    QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
    <>c__DisplayClass9_0`1.<Execute>b__0()
    CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func`1 compiler)
    CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
    QueryCompiler.Execute[TResult](Expression query)
    EntityQueryProvider.Execute[TResult](Expression expression)
    EntityQueryable`1.GetEnumerator()
    List`1.ctor(IEnumerable`1 collection)
    Enumerable.ToList[TSource](IEnumerable`1 source)
    NullCoalescingTests.ShouldTranslateProjectionOnNullCoalescedEntity() line 41
Further technical details

EF Core version: 3.1.2
Database provider: Microsoft.EntityFrameworkCore.SqlServer 3.1.2
Target framework: .NET Core 3.1
Operating system: Windows 10 x64
IDE: Visual Studio 2019 16.4

Dominant language
C#
Stars
14.8k
Forks
3.4k
Avg merge
2d 5h
Merged PRs (30d)
134

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/efcore

All issues in dotnet/efcore

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.