Home > Net >  C# EF Core build dynamic select statement with expression tree
C# EF Core build dynamic select statement with expression tree

Time:04-22

I want to dynamically create a select statement that creates an array of objects through an array initializer. Those initializers are taken from a provided list of property expressions.

In this example we want to list just the 'Component' property of an entity called 'topic'.

This is how the select statement should look like:

Query.Select(topic => new object[] { topic.Component });

And here is how I create that expression dynamically:

// an example expression to be used. We only need its body: topic.Component
Expression<Func<Topic, object>> providedExpression = topic => topic.Component;
            
// a list of the initializers: new object[] { expression 1, expression 2, ..}. We only use the example expression here
List<Expression> initializers = new List<Expression>() { providedExpression.Body };
            
// the expression: new object[] {...} 
NewArrayExpression newArrayExpression = Expression.NewArrayInit(typeof(object), initializers);

// the expression topic => 
var topicParam = Expression.Parameter(typeof(Topic), "topic"); 
            
// the full expression  topic => new object[] { ... };
Expression<Func<Topic, object[]>> lambda = Expression.Lambda<Func<Topic, object[]>>(newArrayExpression, topicParam);

// pass the expression
Query.Select(lambda);

Now, the created expression looks exactly like the example above, but EF Core throws the good old

The LINQ expression 'topic' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly...

But even from the debugger (see image), the (working) example expression and the generated one are identical. Where does the magic happen that I don't understand? Any tips?

Generated and example expression in debugger

CodePudding user response:

The generated and example expressions may look identical in the debugger, but they actually aren’t. The problem is that your lambda expression references two ParameterExpression objects, both named topic:

  1. The first is created implicitly by the C# compiler when it converts topic => topic.Component to an Expression.
  2. The second, topicParam, is created explicitly.

Even though the two ParameterExpression objects have identical names, they’re treated as distinct parameters. To fix the code, you must ensure that the same ParameterExpression object is used in both the parameter list and body of the lambda:

var topicParam = providedExpression.Parameters[0]; // instead of Expression.Parameter

However, if you have multiple provided expressions, then the C# compiler will generate multiple topic ParameterExpression objects, so this simple fix won’t work. Instead, you will need to replace the auto-generated topic parameter in each providedExpression with your explicitly created ParameterExpression:

public class ParameterSubstituter : ExpressionVisitor
{
    private readonly ParameterExpression _substituteExpression;

    public ParameterSubstituter(ParameterExpression substituteExpression)
    {
        _substituteExpression = substituteExpression;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return _substituteExpression;
    }
}

And in your method:

var topicParam = Expression.Parameter(typeof(Topic), "topic");
List<Expression> initializers =
    new List<Expression>
    {
        new ParameterSubstituter(topicParam).Visit(providedExpression.Body)
    };
NewArrayExpression newArrayExpression = Expression.NewArrayInit(typeof(object), initializers);
Expression<Func<Topic, object[]>> lambda = Expression.Lambda<Func<Topic, object[]>>(newArrayExpression, topicParam);
  • Related