'Argument types do not match' when selecting off of a projection with a null check
Repro: https://github.com/MrZander/ProjectionArgExceptionRepro/tree/no-automapper
I have the following queries:
//Works
var test1 = context.Entities
.Select(f => new DtoEntity()
{
ID = f.ID,
Type = new DtoEntityType()
{
ID = f.Type.ID,
Tags = f.Type.Tags.Select(t => new DtoTag() { ID = t.ID })
}
})
.Select(f => new FinalResult
{
Value = f.Type.Tags.Count()
})
.Single();
Console.WriteLine(test1.Value);
//Exception
var test2 = context.Entities
.Select(f => new DtoEntity()
{
ID = f.ID,
Type = f.Type == null ? null : new DtoEntityType()
{
ID = f.Type.ID,
Tags = f.Type.Tags.Select(t => new DtoTag() { ID = t.ID })
}
})
.Select(f => new FinalResult
{
Value = f.Type.Tags.Count()
})
.Single();
Console.WriteLine(test2.Value);
The only difference is the null check in the second query f.Type == null ? null : new DtoEntityType()
The top query executes as expected, the bottom query throws this exception:
System.ArgumentException
HResult=0x80070057
Message=Argument types do not match
Source=System.Linq.Expressions
StackTrace:
at System.Linq.Expressions.Expression.Condition(Expression test, Expression ifTrue, Expression ifFalse, Type type)
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryProjectionBindingExpressionVisitor.VisitConditional(ConditionalExpression conditionalExpression)
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryProjectionBindingExpressionVisitor.Visit(Expression expression)
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryProjectionBindingExpressionVisitor.VisitUnary(UnaryExpression unaryExpression)
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryProjectionBindingExpressionVisitor.Visit(Expression expression)
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryProjectionBindingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryProjectionBindingExpressionVisitor.Visit(Expression expression)
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryProjectionBindingExpressionVisitor.VisitUnary(UnaryExpression unaryExpression)
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryProjectionBindingExpressionVisitor.Visit(Expression expression)
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryProjectionBindingExpressionVisitor.VisitMemberAssignment(MemberAssignment memberAssignment)
at System.Linq.Expressions.ExpressionVisitor.VisitMemberBinding(MemberBinding node)
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryProjectionBindingExpressionVisitor.VisitMemberInit(MemberInitExpression memberInitExpression)
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryProjectionBindingExpressionVisitor.Visit(Expression expression)
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryProjectionBindingExpressionVisitor.Translate(InMemoryQueryExpression queryExpression, Expression expression)
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryQueryableMethodTranslatingExpressionVisitor.TranslateSelect(ShapedQueryExpression source, LambdaExpression selector)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.Translate(Expression expression)
at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass9_0`1.<Execute>b__0()
at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
at Program.Main(String[] args) in C:\<redacted>\ProjectionArgExceptionRepro\ProjectionArgExceptionRepro\Program.cs:line 101
The expected result would be that the two queries result in identical execution. My hunch is that whatever internal mechanism is determining nullability is getting confused by the null check on that the EntityType.
EF Core version: 8.0.2 Database provider: Microsoft.EntityFrameworkCore.SqlServer (The repro uses in-memory for ease of replication) Target framework: .NET 8.0 Operating system: Windows 10 IDE:Visual Studio 2022 17.10.3
Still repros for SQL Server on EF9. Doesn't repro if the second projection to the FinalResult type is removed.
Code
using (var context = new ReproContext())
{
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();
context.Entities.Add(new Entity
{
Type = new EntityType
{
Tags = new List<Tag> { new() }
}
});
await context.SaveChangesAsync();
var test2 = await context.Entities
.Select(f => new DtoEntity
{
ID = f.ID,
Type = f.Type == null
? null
: new DtoEntityType
{
ID = f.Type.ID,
Tags = f.Type.Tags.Select(t => new DtoTag { ID = t.ID })
}
})
.Select(f => new FinalResult
{
Value = f.Type.Tags.Count()
})
.SingleAsync();
}
public class ReproContext : DbContext
{
public DbSet<Entity> Entities { get; set; }
public DbSet<EntityType> EntityTypes { get; set; }
public DbSet<Tag> Tags { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.EnableSensitiveDataLogging()
.LogTo(Console.WriteLine, LogLevel.Information)
.UseSqlServer(
"Server=localhost;Database=DoomSlug;Trusted_Connection=True;TrustServerCertificate=True");
}
public class Entity
{
public int ID { get; set; }
public EntityType Type { get; set; }
}
public class EntityType
{
public int ID { get; set; }
public IEnumerable<Tag> Tags { get; set; }
}
public class Tag
{
public int ID { get; set; }
}
public class DtoEntity
{
public int ID { get; set; }
public DtoEntityType Type { get; set; }
}
public class DtoEntityType
{
public int ID { get; set; }
public IEnumerable<DtoTag> Tags { get; set; }
}
public class DtoTag
{
public int ID { get; set; }
}
public class FinalResult
{
public decimal Value { get; set; }
}