zzzprojects / System.Linq.Dynamic.Core

The .NET Standard / .NET Core version from the System Linq Dynamic functionality.
https://dynamic-linq.net/
Apache License 2.0
1.57k stars 228 forks source link

SelectMany on ICollection property throws ArgumentException on EF Core 7 #701

Open jbhelm opened 1 year ago

jbhelm commented 1 year ago

1. Description

When calling SelectMany() within a Dynamic LINQ expression for an ICollection<> property, an ArgumentException is thrown when executed on EF Core 7 LINQ to Entities. Note that it does work on LINQ to Objects, LINQ to Entities with EF 6 (classic), and on EF Core 7 if the properties are declared as IEnumerable<> instead of ICollection<>.

2. Exception

System.ArgumentException
  HResult=0x80070057
  Message=Expression of type 'System.Linq.Expressions.Expression`1[System.Func`2[DynamicLinqEfCoreExample.Program+Child,System.Collections.Generic.ICollection`1[DynamicLinqEfCoreExample.Program+Grandchild]]]' cannot be used for parameter of type 'System.Linq.Expressions.Expression`1[System.Func`2[DynamicLinqEfCoreExample.Program+Child,System.Collections.Generic.IEnumerable`1[DynamicLinqEfCoreExample.Program+Grandchild]]]' of method 'System.Linq.IQueryable`1[DynamicLinqEfCoreExample.Program+Grandchild] SelectMany[Child,Grandchild](System.Linq.IQueryable`1[DynamicLinqEfCoreExample.Program+Child], System.Linq.Expressions.Expression`1[System.Func`2[DynamicLinqEfCoreExample.Program+Child,System.Collections.Generic.IEnumerable`1[DynamicLinqEfCoreExample.Program+Grandchild]]])' (Parameter 'arg1')
  Source=System.Linq.Expressions
  StackTrace:
   at System.Dynamic.Utils.ExpressionUtils.ValidateOneArgument(MethodBase method, ExpressionType nodeKind, Expression arguments, ParameterInfo pi, String methodParamName, String argumentParamName, Int32 index)
   at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, Expression arg0, Expression arg1)
   at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, IEnumerable`1 arguments)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryableMethodNormalizingExpressionVisitor.TryConvertEnumerableToQueryable(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryableMethodNormalizingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.ExpressionVisitor.VisitLambda[T](Expression`1 node)
   at System.Linq.Expressions.ExpressionVisitor.VisitUnary(UnaryExpression node)
   at System.Dynamic.Utils.ExpressionVisitorUtils.VisitArguments(ExpressionVisitor visitor, IArgumentProvider nodes)
   at System.Linq.Expressions.ExpressionVisitor.VisitMethodCall(MethodCallExpression node)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryableMethodNormalizingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryableMethodNormalizingExpressionVisitor.Normalize(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.QueryTranslationPreprocessor.NormalizeQueryableMethod(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.QueryTranslationPreprocessor.Process(Expression query)
   at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryQueryTranslationPreprocessor.Process(Expression query)
   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 Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetEnumerator()
   at System.Collections.Generic.LargeArrayBuilder`1.AddRange(IEnumerable`1 items)
   at System.Collections.Generic.EnumerableHelpers.ToArray[T](IEnumerable`1 source)
   at System.Linq.Dynamic.Core.DynamicEnumerableExtensions.CastToArray[T](IEnumerable source)
   at System.Linq.Dynamic.Core.DynamicEnumerableExtensions.ToDynamicArray(IEnumerable source)
   at DynamicLinqEfCoreExample.Program.Main(String[] args) in C:\Users\JoshuaHelm\dev\DynamicLinqEfCoreExample\Program.cs:line 40

3. Fiddle or Project

using Microsoft.EntityFrameworkCore;
using System.Linq.Dynamic.Core;

namespace DynamicLinqEfCoreExample
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var options = new DbContextOptionsBuilder<ExampleContext>().UseInMemoryDatabase(databaseName: "Example").Options;

            using (var context = new ExampleContext(options))
            {
                Root root = new()
                {
                    Id = 1,
                    Children = new Child[] {
                        new() {
                            Id = 2,
                            Grandchildren = new Grandchild[]
                            {
                                new()
                                {
                                    Id = 3,
                                }
                            }
                        }
                    }
                };
                context.Roots.Add(root);
                context.SaveChanges();

                // LINQ to Objects (works)
                var data = new[] { root }.AsQueryable();
                var a = data.Select(r => r.Children.SelectMany(c => c.Grandchildren)).ToArray();    // <-- works
                var b = data.Select("Children.SelectMany(Grandchildren)").ToDynamicArray();         // <-- works

                // LINQ to Entities (fails)
                var c = context.Roots.Select(r => r.Children.SelectMany(c => c.Grandchildren)).ToArray();    // <-- works
                var d = context.Roots.Select("Children.SelectMany(Grandchildren)").ToDynamicArray();         // <-- throws error (works if Children and Grandchildren properties are defined as IEnumerable instead of ICollection)
            }

        }

        public class Root
        {
            public int Id { get; set; }
            public ICollection<Child> Children { get; set; } = new HashSet<Child>();    // <-- dynamic LINQ query works if this and Child.Grandchildren are IEnumerable instead of ICollection
        }

        public class Child
        {
            public int Id { get; set; }
            public ICollection<Grandchild> Grandchildren { get; set; } = new HashSet<Grandchild>(); // <-- dynamic LINQ query works if this and Root.Children are IEnumerable instead of ICollection
        }

        public class Grandchild
        {
            public int Id { get; set; }
        }

        public class ExampleContext : DbContext
        {
            public ExampleContext() : base()
            {
            }

            public ExampleContext(DbContextOptions<ExampleContext> options)
                : base(options)
            {
            }

            public DbSet<Root> Roots { get; set; }

        }
    }
}

4. Any further technical details

Thanks!

StefH commented 1 year ago

@jbhelm I'm also able to reproduce this using .NET 6 and EF Core 6...

jbhelm commented 1 year ago

Hi @StefH

I didn't try it with any earlier versions of EF Core. However, I can say it does work with "classic" EntityFramework 6.4.4 on .NET 7.

jbhelm commented 2 months ago

Just want to add to this that I've found, as a work around, that adding Select(it) to the SelectMany() expression works. For example change:

context.Roots.Select("Children.SelectMany(Grandchildren)").ToDynamicArray()  // <-- throws

to

context.Roots.Select("Children.SelectMany(Grandchildren.Select(it))").ToDynamicArray();  // <-- works
jbhelm commented 2 months ago

...or even better, use AsEnumerable():

context.Roots.Select("Children.SelectMany(Grandchildren.AsEnumerable())").ToDynamicArray();  // <-- works