C# EF Core 构建带有表达式树的动态 select 语句

C# EF Core build dynamic select statement with expression tree

我想动态创建一个 select 语句,通过数组初始值设定项创建一个对象数组。这些初始值设定项取自提供的 属性 表达式列表。

在此示例中,我们只想列出名为 'topic' 的实体的 'Component' 属性。

select 语句应该是这样的:

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

下面是我如何动态创建该表达式:

// 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);

现在,创建的表达式看起来与上面的示例完全一样,但是 EF Core 抛出了旧的

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...

但即使在调试器中(见图),(工作)示例表达式和生成的表达式也是 相同。我不明白的魔法在哪里发生? 有什么建议吗?

Generated and example expression in debugger

生成的表达式和示例表达式在调试器中可能看起来相同,但实际上并非如此。问题是您的 lambda 表达式引用了 两个 ParameterExpression 对象,它们都命名为 topic:

  1. 第一个是由 C# 编译器在将 topic => topic.Component 转换为表达式时隐式创建的。
  2. 第二个 topicParam 是显式创建的。

即使两个 ParameterExpression 对象具有相同的名称,它们也被视为不同的参数。要修复代码,您必须确保 相同的 ParameterExpression 对象用于 lambda:

的参数列表和正文
var topicParam = providedExpression.Parameters[0]; // instead of Expression.Parameter

但是,如果您有多个提供的表达式,那么 C# 编译器将生成多个 topic ParameterExpression 对象,因此这个简单的修复将不起作用。相反,您需要将每个 providedExpression 中的 auto-generated topic 参数替换为您明确创建的 ParameterExpression:

public class ParameterSubstituter : ExpressionVisitor
{
    private readonly ParameterExpression _substituteExpression;

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

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

在你的方法中:

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);