koenbeuk / EntityFrameworkCore.Projectables

Project over properties and functions in your linq queries
MIT License
260 stars 17 forks source link

Nested collection queries: System.InvalidOperationException: Unable to resolve generated expression #94

Closed benbristow closed 4 months ago

benbristow commented 8 months ago

Some very odd behaviour I've found recently whilst using this library on a project. Rather hard to explain.

I've put an example project together which when ran will trigger an exception just like in our project's code:

https://github.com/benbristow/entityframework-projectables-bug

The main affected code:

Console.WriteLine("test 1");
await context.Students
    .Select(s =>
        new
        {
            CommentCount = s.Comments.SearchComment("foo").Count(),
            CourseCount = s.Courses.SearchCourse("bar").Count(),
        })
    .ToListAsync();
Console.WriteLine("pass 1");

Console.WriteLine("test 2");
await context.Students
    .Select(s =>
        new
        {
            CommentCount = s.Comments.Search("foo").Count(),
            CourseCount = s.Courses.Search("bar").Count(),
        })
    .ToListAsync();
Console.WriteLine("pass 2");
System.InvalidOperationException: Unable to resolve generated expression for ProjectablesExample.Extensions.IHasNameExtensions.SearchComment.
   at EntityFrameworkCore.Projectables.Services.ProjectionExpressionResolver.FindGeneratedExpression(MemberInfo projectableMemberInfo)
   at EntityFrameworkCore.Projectables.Services.ProjectableExpressionReplacer.TryGetReflectedExpression(MemberInfo memberInfo, LambdaExpression& reflectedExpression)
   at EntityFrameworkCore.Projectables.Services.ProjectableExpressionReplacer.VisitMethodCall(MethodCallExpression node)
   at System.Dynamic.Utils.ExpressionVisitorUtils.VisitArguments(ExpressionVisitor visitor, IArgumentProvider nodes)
   at System.Linq.Expressions.ExpressionVisitor.VisitMethodCall(MethodCallExpression node)
   at EntityFrameworkCore.Projectables.Services.ProjectableExpressionReplacer.VisitMethodCall(MethodCallExpression node)
   at System.Dynamic.Utils.ExpressionVisitorUtils.VisitArguments(ExpressionVisitor visitor, IArgumentProvider nodes)
   at System.Linq.Expressions.ExpressionVisitor.VisitNew(NewExpression node)
   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 EntityFrameworkCore.Projectables.Services.ProjectableExpressionReplacer.VisitMethodCall(MethodCallExpression node)
   at EntityFrameworkCore.Projectables.Infrastructure.Internal.CustomQueryCompiler.Expand(Expression expression)
   at EntityFrameworkCore.Projectables.Infrastructure.Internal.CustomQueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetAsyncEnumerator(CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.ConfiguredCancelableAsyncEnumerable`1.GetAsyncEnumerator()
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
   at Program.<Main>$(String[] args) in C:\Users\BenjaminBristow\dev\misc\ProjectablesExample\ProjectablesExample\Program.cs:line 29
   at Program.<Main>(String[] args)

Now if I remove the [Projectable] attribute from all of the extension methods in the below file, the program runs to completion with no issues (I don't think these examples actually need this library as EF seems to compile queries OK, but is different in our project where this library is required but to seems to be the same issue).

https://github.com/benbristow/entityframework-projectables-bug/blob/master/ProjectablesExample/Extensions/IHasNameExtensions.cs

If I leave the [Projectable] attribute on the uniquely named methods as below, the program also runs to completion with no issues, e.g.

    public static IEnumerable<Comment> Search(this IEnumerable<Comment> source, string search)
        => source.Where(s => s.Text == search);

    [Projectable]
    public static IEnumerable<Comment> SearchComment(this IEnumerable<Comment> source, string search)
        => source.Where(s => s.Text == search);

    public static IEnumerable<Course> Search(this IEnumerable<Course> source, string search)
        => source.Where(s => s.Name == search);

    [Projectable]
    public static IEnumerable<Course> SearchCourse(this IEnumerable<Course> source, string search)
        => source.Where(s => s.Name == search);

However, if I swap this around, it generates the same exception

    [Projectable]
    public static IEnumerable<Comment> Search(this IEnumerable<Comment> source, string search)
        => source.Where(s => s.Text == search);

    public static IEnumerable<Comment> SearchComment(this IEnumerable<Comment> source, string search)
        => source.Where(s => s.Text == search);

    [Projectable]
    public static IEnumerable<Course> Search(this IEnumerable<Course> source, string search)
        => source.Where(s => s.Name == search);

    public static IEnumerable<Course> SearchCourse(this IEnumerable<Course> source, string search)
        => source.Where(s => s.Name == search);

My hypothesis here is that I think there's an issue where the library isn't picking up the generic argument as being a different method and is getting confused.

Example run: https://github.com/benbristow/entityframework-projectables-bug/actions/runs/6788027570/job/18452129581

koenbeuk commented 8 months ago

Thanks for reporting this and apologies for not coming back to this earlier!

The problem is due to method overloading. This is currently not supported by this library. Each method projected with a Projectable attribute with generate a companion expression in a new source file. The name of his generated source file is currently derived roughly as: {Namespace}_{ClassName}_{MethodName}. When overloading methods, these generated source files are no longer unique which will cause generation to fail in which case during runtime, we'll be unable to locate the generated expression.

We should ensure that the generated file names are unique. One way would be to include the line / column of actual method that we generate a projectable expression for as part of the file name.

Meanwhile the workaround would be to not use method overloading and give each method a unique name.

koenbeuk commented 4 months ago

This is tracked by #58