LinqKit 的扩展器无法从字段中获取表达式是不是有特殊原因?

Posted

技术标签:

【中文标题】LinqKit 的扩展器无法从字段中获取表达式是不是有特殊原因?【英文标题】:Is there a particular reason LinqKit's expander can't pick up Expressions from fields?LinqKit 的扩展器无法从字段中获取表达式是否有特殊原因? 【发布时间】:2011-09-07 17:47:46 【问题描述】:

我正在使用LinqKit 库,它允许动态组合表达式。

这对于编写 Entity Framewok 数据访问层来说是一种纯粹的幸福,因为可以选择性地重用和组合多个表达式,这使得代码既可读又高效。

考虑以下代码:

private static readonly Expression<Func<Message, int, MessageView>> _selectMessageViewExpr =
    ( Message msg, int requestingUserId ) =>
        new MessageView
        
            MessageID = msg.ID,
            RequestingUserID = requestingUserId,
            Body = ( msg.RootMessage == null ) ? msg.Body : msg.RootMessage.Body,
            Title = ( ( msg.RootMessage == null ) ? msg.Title : msg.RootMessage.Title ) ?? string.Empty
        ;

我们声明了一个将Message 投影到MessageView 上的表达式(为了清楚起见,我删除了细节)。

现在,数据访问代码可以使用这个表达式来获取单个消息:

var query = CompiledQueryCache.Instance.GetCompiledQuery(
    "GetMessageView",
    () => CompiledQuery.Compile(
        _getMessagesExpr
            .Select( msg => _selectMessageViewExpr.Invoke( msg, userId ) ) // re-use the expression
            .FirstOrDefault( ( MessageView mv, int id ) => mv.MessageID == id )
            .Expand()
        )
    );

这很漂亮,因为同样的表达式也可以用于获取消息列表:

var query = CompiledQueryCache.Instance.GetCompiledQuery(
    "GetMessageViewList",
    () => CompiledQuery.Compile(
        BuildFolderExpr( folder )
            .Select( msg => _selectMessageViewExpr.Invoke( msg, userId ) )
            .OrderBy( mv => mv.DateCreated, SortDirection.Descending )
            .Paging()
            .Expand()
        ),
    folder
    );

如您所见,投影表达式存储在_selectMessageViewExpr 中,用于构建多个不同的查询。

但是,我花了很多时间跟踪一个奇怪的错误,这段代码在 Expand() 调用时崩溃。 错误说:

无法将 System.Linq.Expressions.FieldExpression 类型的对象转换为 System.Linq.Expressions.LambdaExpression 类型。

过了一段时间我才意识到在调用Invoke之前在局部变量中引用表达式时一切正常

var selector = _selectMessageViewExpr; // reference the field

var query = CompiledQueryCache.Instance.GetCompiledQuery(
    "GetMessageView",
    () => CompiledQuery.Compile(
        _getMessagesExpr
            .Select( msg => selector.Invoke( msg, userId ) ) // use the variable
            .FirstOrDefault( ( MessageView mv, int id ) => mv.MessageID == id )
            .Expand()
        )
    );

代码按预期工作。

我的问题是:

LinqKit 不能识别存储在字段中的表达式上的Invoke 有什么具体原因吗? 只是开发人员的遗漏,还是有一些重要的原因需要存储表达式首先在局部变量中?

这个问题可能可以通过查看生成的代码和检查 LinqKit 源来回答,但是我想也许与 LinqKit 开发相关的人可以回答这个问题。

谢谢。

【问题讨论】:

你找到答案了吗? @Lawrence:不,我还是很好奇。目前我使用描述的解决方法。 我刚刚在我的项目中遇到了这个问题。据我从 LinqKit 源代码中可以看出,这似乎是因为 ExpressionExpander 没有被编程为处理属性字段。它不知道如何调用“get”,因为它基于反射工作。我正在尝试找出一个解决方案,所以我把它加入书签 @Charles:酷。我实际上没想到有人会回答或评论这个问题,所以看到回复很有趣。 +1,直到看到您的问题,我才意识到为什么会出现错误,这使我意识到我需要先将其存储在局部变量中。谢谢! 【参考方案1】:

我下载了源代码并尝试分析它。 ExpressionExpander 不允许引用存储在常量以外的变量中的表达式。它期望调用Invoke 方法来引用ConstantExpression 表示的对象,而不是另一个MemberExpression

所以我们不能提供我们的可重用表达式作为对类的任何成员的引用(即使是公共字段,而不是属性)。也不支持嵌套成员访问(如object.member1.member2 ...等)。

但这可以通过遍历初始表达式并递归提取子字段值来解决。

我已将ExpressionExpander类的TransformExpr方法代码替换为

var lambda = Expression.Lambda(input);
object value = lambda.Compile().DynamicInvoke();

if (value is Expression)
    return Visit((Expression)value);
else
    return input;

现在可以使用了。

在这个解决方案中,我之前提到的所有内容(递归遍历树)都是由 ExpressionTree 编译器为我们完成的 :)

【讨论】:

一个地狱般的答案。老实说,我根本没想到会回答这个问题。谢谢。 附带说明,你并不需要 JS 来格式化——只要确保你熟悉 Markdown。你可以看到我的编辑,开始。再次感谢! 谢谢:) 我必须说我没有检查所有案例和表达式树,所以我不能保证它会正常工作。也许进一步的调整是必须的。 LinqKit 代码比较复杂:) 我试用了您的代码,虽然它确实让我克服了 LinqKit 的错误,但 ELinq 现在给了我以下错误。有任何想法吗?以下是错误:“无法创建类型为'...'的空常量值。在此上下文中仅支持实体类型、枚举类型或原始类型。” 使用您的解决方案后的麦克风我遇到了另一个异常:variable 'p' of type '(...)' referenced from scope '', but it is not defined。解决方案是跳过一些输入:if (input == null || (input.NodeType == ExpressionType.MemberAccess &amp;&amp; !(input.Member is FieldInfo))) return input;【参考方案2】:

我创建了Mic answer的改进版:

if (input == null)
    return input;

var field = input.Member as FieldInfo;
var prope = input.Member as PropertyInfo;
if ((field != null && field.FieldType.IsSubclassOf(typeof(Expression))) ||
    (prope != null && prope.PropertyType.IsSubclassOf(typeof(Expression))))
    return Visit(Expression.Lambda<Func<Expression>>(input).Compile()());

return input;

主要优点是删除具有big overhead 的DynamicInvoke 并仅在我真正需要时调用Invoke

【讨论】:

以上是关于LinqKit 的扩展器无法从字段中获取表达式是不是有特殊原因?的主要内容,如果未能解决你的问题,请参考以下文章

如何从 Debugger.GetExpression 获取 DisplayString 字段?

在 AWS 解析器映射模板中,是不是有任何方法可以从对象中删除字段?

EntityFramework扩展之第三方类库

如何从 GraphQL 查询中获取字段值?

JPA HSQL 不从扩展类中获取字段

使用更多表单字段扩展奏鸣曲用户包,获取无法加载类型“Application\Sonata\UserBundle\Form\RegistrationType”