Search code examples
.net-corelambdareflectionef-core-3.1

Constructing expression tree to call Any method on a IQueryable collection (nested query)


I'm trying to build a extension method to IQueryable that will check if there is any matching elements in a navigation property. What I do outside of the code below is to iterate over the properties of a type and check whether or not the property is a navigation property using IModel.

I'm having problems trying to figure out how to build the expression tree for the one-to-many relationships. What I want to achieve is something like Entity.Collections.Any(Collection => Collection.Name == 'Filter'), which would then be a where clause of the main query. The inner clause is built up outside of the sample below, but I believe the error occurs before reaching that expression.

I think that the delegate type of the Lambda is probably constructed wrong as it's not returning bool, it's returning IEnumerable<Collection>, but I can't figure out how it should be constructed if not like this.

I'm trying to follow this guide from the Microsoft docs.

The lines of code that is trying to solve what's mentioned above:

if (typeof(IEnumerable).IsAssignableFrom(targetType))
{
    var targetEntity = targetType.GetGenericArguments().First();
    var subArgument = Expression.Parameter(targetEntity, targetEntity.Name);

    var delegateType = typeof(Func<,>).MakeGenericType(targetEntity, typeof(bool));
    var lambda = Expression.Lambda(delegateType, innerClause, Expression.Parameter(targetEntity));

    Expression.Call(
        typeof(Queryable),
        "Any",
        new Type[] { targetEntity },
        Expression.Property(argument, property.Name),
        lambda
    );
}

I get the below error at the line where I'm trying to create the Expression.Call.

Exception thrown: 'System.InvalidOperationException' in System.Linq.Expressions.dll: 'No generic method 'Any' on type 'System.Linq.Queryable' is compatible with the supplied type arguments and arguments. No type arguments should be provided if the method is non-generic. '

I've also tried to get the any method and invoking it:

var anyMethod = typeof(Queryable)
    .GetMethods()
    .Where(m => m.Name == "Any" && m.IsGenericMethodDefinition)
    .Where(m => m.GetParameters().ToList().Count == 2)
    .First();

var genericMethod = anyMethod.MakeGenericMethod(entityType);

genericMethod.Invoke(Expression.Property(argument, property.Name), new object[] { innerClause, subArgument });

And then I get this error instead:

Exception thrown: 'System.ArgumentException' in System.Private.CoreLib.dll: 'Object of type 'System.Linq.Expressions.MethodCallExpression3' cannot be converted to type 'System.Linq.IQueryable1[Collection]'.'`


Solution

  • This is how I solved it:

    if (typeof(IEnumerable).IsAssignableFrom(targetType))
    {
        var targetEntity = targetType.GetGenericArguments().First();
        var subArgument = Expression.Parameter(targetEntity, targetEntity.Name);
    
        var anyMethod = typeof(Enumerable)
            .GetMethods()
            .Where(m => m.Name == "Any" && m.IsGenericMethodDefinition)
            .Where(m => m.GetParameters().ToList().Count == 2)
            .First();
    
        anyMethod = GetAnyMethod().MakeGenericMethod(targetType);
    
        Expression.Call(anyMethod, Expression.MakeMemberAccess(argument, property), Expression.Lambda(x, subArgument));
    }
    

    So, the main problem was that I was trying to call the method on Queryable rather than Enumerable, which I should have since I was calling the method on a property rather than the queryable object.