DarkLeafyGreen
DarkLeafyGreen

Reputation: 70406

ArgumentException when trying to translate efcore expression to expression trees

I am trying to build following query

_dbContext.Models.Where(m => m.Children.Any(c => c.Name.Contains("Foobar")))

using expression trees. I am not very familar with expression trees yet, quite frankly I find them challenging. What I've tried:

var toCompare = "Foobar";
var param = Expression.Parameter(typeof(T));

// Properties

var propertyLeft = Expression.Property(param, "Children");
// Trying to retrieve Name property of the Child type
var propertyRight = Expression.Property(Expression.Parameter(propertyLeft.Type.GetGenericArguments().Single()), "Name");

// Methods

var contains = typeof(string).GetMethod("Contains", new[] { typeof(string) })!;

var any = typeof(Queryable).GetMethods()
    .Where(m => m.Name == "Any")
    .First(m => m.GetParameters().Length == 2)
    .MakeGenericMethod(typeof(string));

// Build c => c.Name.Contains(toCompare)

var expContains = Expression.Call(propertyRight, contains, Expression.Constant(toCompare, propertyRight.Type));
var expContainsLambda = Expression.Lambda<Func<T, bool>>(expContains, parameterExpression);

// Build m => m.Children.Any(...)

var expAny = Expression.Call(any, Expression.Constant(toCompare), expContainsLambda);
var expAnyLambda = Expression.Lambda<Func<Expression<Func<T, string>>, bool>>(expAny, parameterExpression);

// Build Models.Where(m => ...)

var expWhere = Expression.Lambda<Func<T, bool>>(expAnyLambda, param);

I am getting following error at expAny

System.ArgumentException: Expression of type 'System.String' cannot be used for parameter of type 'System.Linq.IQueryable`1[System.String]' of method 'Boolean Any[String](System.Linq.IQueryable`1[System.String], System.Linq.Expressions.Expression`1[System.Func`2[System.String,System.Boolean]])' (Parameter 'arg0')

Any ideas why? What changes are necessary to make it work?

Upvotes: 1

Views: 156

Answers (2)

Zev Spitz
Zev Spitz

Reputation: 15297

(Disclaimer: I am the author of the library in question.)

I've written a library that produces various string representations from expression trees, such as C# or VB code, or Dynamic LINQ. One of the representations is the factory methods needed to create an expression tree. For example, this:

Expression<Func<Model, bool>> expr = m => m.Children.Any(c => c.Name.Contains("Foobar"));
Console.WriteLine(
    expr.ToString("Factory methods", "C#")
);

produces this (using simulated types Model and ModelChild:

// using static System.Linq.Expressions.Expression

var c = Parameter(
    typeof(ModelChild),
    "c"
);
var m = Parameter(
    typeof(Model),
    "m"
);

Lambda(
    Call(
        typeof(Queryable).GetMethod("Any", new[] { typeof(IQueryable<ModelChild>), typeof(Expression<Func<ModelChild, bool>>) }),
        MakeMemberAccess(m,
            typeof(Model).GetProperty("Children")
        ),
        Quote(
            Lambda(
                Call(
                    MakeMemberAccess(c,
                        typeof(ModelChild).GetProperty("Name")
                    ),
                    typeof(string).GetMethod("Contains", new[] { typeof(string) }),
                    Constant("Foobar")
                ),
                c
            )
        )
    ),
    m
)

which you can then use to customize for your own needs.

Upvotes: 0

Svyatoslav Danyliv
Svyatoslav Danyliv

Reputation: 27282

Try the following:

var toCompare = "Foobar";

var param = Expression.Parameter(typeof(T), "m");

// m.Children
var propertyLeft = Expression.Property(param, "Children");

var childType = propertyLeft.Type.GetGenericArguments()[0];
var childParam = Expression.Parameter(childType, "c");

// c.Name
var propertyRight = Expression.Property(childParam, "Name");

// c => c.Name.Contains("Foobar")
var anyPredicate = 
    Expression.Lambda(
        Expression.Call(propertyRight, nameof(string.Contains), Type.EmptyTypes, Expression.Constant(toCompare)),
        childParam
    )

// m.Children.Any(c => c.Name.Contains("Foobar"))
var anyCall = Expression.Call(typeof(Enumerable), nameof(Enumerable.Any), new [] { childType }, propertyLeft, anyPredicate);

// m => m.Children.Any(c => c.Name.Contains("Foobar"))
var expWhere = Expression.Lambda<Func<T, bool>>(anyCall, param);

Upvotes: 1

Related Questions