Search code examples
c#linq.net-coreentity-framework-coreexpression-trees

Correct Collection in ConstantExpression for EF Core


I try to realize my own Expression serializator/deserializator for passing it through services (I want to realize my own endpoint for EF Core service). So, now I have problem with Collections in LambdaExpressions. For example,

var dataQuery = testDb.Users.Include(e => e.EmployeeInfo).Include(f => f.Notifications).Where(s => tstList.Contains(s.Id)).Select(e => e.FullName);
var tstEspressionBase = dataQuery.Expression;
var tstEspression = new ReflectionLocalValculationVisitor().Visit(tstEspressionBase);

here

public class ReflectionLocalValculationVisitor : ExpressionVisitor
{
    protected override Expression VisitMember(MemberExpression memberExpression)
    {
        var expression = Visit(memberExpression.Expression);

        if (expression is ConstantExpression)
        {
            object container = ((ConstantExpression)expression).Value;
            var member = memberExpression.Member;
            if (member is FieldInfo)
            {
                object value = ((FieldInfo)member).GetValue(container);
                return Expression.Constant(value);
            }
            if (member is PropertyInfo)
            {
                object value = ((PropertyInfo)member).GetValue(container, null);
                return Expression.Constant(value);
            }
        }
        return base.VisitMember(memberExpression);
    }
}

var tstList = new List<Guid>()
{
   new Guid("D45E1A1A-F546-48DB-77BA-08D7775C6A93"),
   new Guid("5B21C782-9B95-48F2-77BD-08D7775C6A93")
};

Executing with this code

var providerAsync = testDb.GetService<IAsyncQueryProvider>();
var toListAsyncMethodInfo = typeof(EntityFrameworkQueryableExtensions).GetMethod(nameof(EntityFrameworkQueryableExtensions.ToListAsync)).MakeGenericMethod(typeof(string));
var s3 = await toListAsyncMethodInfo.InvokeAsync(null, new object[] { providerAsync.CreateQuery(tstEspression), default(CancellationToken) }).ConfigureAwait(false);

gives me correct result.

So, after serializing/deserializing with Newtonsoft Json I have a problem with collection in the Lambda from Where method:

Error Message: The LINQ expression 'DbSet .Where(u => List { d45e1a1a-f546-48db-77ba-08d7775c6a93, 5b21c782-9b95-48f2-77bd-08d7775c6a93, }.Contains(s.Id))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

I tried to realize this "recommendation", but with no effect (see the code below):

var asEnumerableMethod = typeof(Enumerable).GetMethod(nameof(Enumerable.AsEnumerable)).MakeGenericMethod(GenericTypes.Select(e => e.FromNode()).ToArray());
var asEnumerabled = asEnumerableMethod.Invoke(null, new object[] { Value });

here Value object is a List<Guid> generated by JSON.NET after deserialization. So, I compared realized interfaces for Value in ConstantExpression which represents List<guid> before serialization and after deserialization - both implements 8 interfaces.

So, maybe someone had the same problem.

Thanks.

P.S. I don't know why EF Core givse me Where(u => ... and not Where(s => ..., because in the DebugView mode for this Expression I see correct Where(s => ... representation.

Let's see the serialized/(deserialized and restored) Expression (data from DebugView):

.Call System.Linq.Queryable.Select(
    .Call System.Linq.Queryable.Where(
        .Call Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.Include(
            .Call Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.Include(
                .Constant<Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[EFCoreDataModel.DataClasses.Users.Base.User]>(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[EFCoreDataModel.DataClasses.Users.Base.User]),
                '(.Lambda #Lambda1<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,EFCoreDataModel.DataClasses.Users.Employ.EmployeeInfo]>))
            ,
            '(.Lambda #Lambda2<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Collections.Generic.ICollection`1[EFCoreDataModel.DataClasses.Notifications.Notification]]>))
        ,
        '(.Lambda #Lambda3<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Boolean]>)),
    '(.Lambda #Lambda4<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.String]>))

.Lambda #Lambda1<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,EFCoreDataModel.DataClasses.Users.Employ.EmployeeInfo]>(EFCoreDataModel.DataClasses.Users.Base.User $e)
{
    $e.EmployeeInfo
}

.Lambda #Lambda2<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Collections.Generic.ICollection`1[EFCoreDataModel.DataClasses.Notifications.Notification]]>(EFCoreDataModel.DataClasses.Users.Base.User $f)
{
    $f.Notifications
}

.Lambda #Lambda3<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Boolean]>(EFCoreDataModel.DataClasses.Users.Base.User $s)
{
    .Call .Constant<System.Collections.Generic.List`1[System.Guid]>(System.Collections.Generic.List`1[System.Guid]).Contains($s.Id)
}

.Lambda #Lambda4<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.String]>(EFCoreDataModel.DataClasses.Users.Base.User $e)
{
    $e.FullName
}

The original Expression (from DebugView):

.Call System.Linq.Queryable.Select(
    .Call System.Linq.Queryable.Where(
        .Call Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.Include(
            .Call Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.Include(
                .Constant<Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[EFCoreDataModel.DataClasses.Users.Base.User]>(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[EFCoreDataModel.DataClasses.Users.Base.User]),
                '(.Lambda #Lambda1<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,EFCoreDataModel.DataClasses.Users.Employ.EmployeeInfo]>))
            ,
            '(.Lambda #Lambda2<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Collections.Generic.ICollection`1[EFCoreDataModel.DataClasses.Notifications.Notification]]>))
        ,
        '(.Lambda #Lambda3<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Boolean]>)),
    '(.Lambda #Lambda4<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.String]>))

.Lambda #Lambda1<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,EFCoreDataModel.DataClasses.Users.Employ.EmployeeInfo]>(EFCoreDataModel.DataClasses.Users.Base.User $e)
{
    $e.EmployeeInfo
}

.Lambda #Lambda2<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Collections.Generic.ICollection`1[EFCoreDataModel.DataClasses.Notifications.Notification]]>(EFCoreDataModel.DataClasses.Users.Base.User $f)
{
    $f.Notifications
}

.Lambda #Lambda3<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.Boolean]>(EFCoreDataModel.DataClasses.Users.Base.User $s)
{
    .Call .Constant<System.Collections.Generic.List`1[System.Guid]>(System.Collections.Generic.List`1[System.Guid]).Contains($s.Id)
}

.Lambda #Lambda4<System.Func`2[EFCoreDataModel.DataClasses.Users.Base.User,System.String]>(EFCoreDataModel.DataClasses.Users.Base.User $e)
{
    $e.FullName
}

So, they are equal. And serialized/deserialized has no u parameter in the Lambda, just 's' as it could be.


Solution

  • Most likely the problem is caused by an unbound lambda expression parameter here after deserialization

    s => tstList.Contains(s.Id)
    

    The condition doesn't really matter. And the debug display is misleading. s in s => is not the same ParameterExpression instance as the s in s.Id. This can't happen with C# compile time expression, but can easily be done using the Expression class methods. Note that from the expression tree point of view, the name of the parameter doesn't matter, just the instance.

    For instance, the following code snippet

    var param1 = Expression.Parameter(typeof(User), "s");
    var param2 = Expression.Parameter(typeof(User), "s");
    var body = Expression.Equal(
        Expression.Property(param2, "Id"),
        Expression.Constant(new Guid("D45E1A1A-F546-48DB-77BA-08D7775C6A93"))
    );
    var predicate = Expression.Lambda<Func<Blog, bool>>(body, param1);
    

    creates a valid (!?) lambda expression, which when used in LINQ query

    var test = testDb.Set<User>().Where(predicate).ToList();
    

    will generate similar exception to the one in question.

    With all that being said, forget about collections in constant expressions and concentrate on lambda expressions to find and fix the code which causes the aforementioned parameter expression discrepancy.