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
:
- The first is created implicitly by the C# compiler when it converts
topic => topic.Component
to an Expression. - 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);