LinqKit的扩展器为什么不能获取来自字段的表达式?

26

我正在使用LinqKit库,该库允许即时组合表达式。

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

考虑以下代码片段:

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 Wagerfield
@Lawrence:不,我还是很好奇。目前我使用了所描述的解决方法。 - Dan Abramov
我在项目中遇到了这个问题。从查看LinqKit源代码来看,似乎是因为ExpressionExpander没有编程处理属性字段。它不知道如何调用“get”,因为它是基于反射工作的。我正在尝试找出解决方案,所以我会将其加入书签。 - Charles
@Charles:很酷。我实际上没想到有人会回答或评论这个问题,所以看到回复还是挺有意思的。 - Dan Abramov
1
+1,直到看到你的问题我才意识到为什么会出现错误,这使我意识到我需要先将其存储在本地变量中。谢谢! - DCShannon
2个回答

25

我下载了源代码并尝试分析它。 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 编译器为我们完成 :)。


真是一份非常好的答案。说实话,我根本没想到会得到答复。非常感谢。 - Dan Abramov
顺便提一下,你不需要JS来进行格式化 - 只需确保熟悉Markdown即可。你可以看看我的编辑,作为一个开始。再次感谢! - Dan Abramov
谢谢:) 我必须说我没有检查所有情况和表达式树,所以我不能保证它总是能正常工作。也许需要进一步调整。LinqKit 代码相当复杂:) - Mic
我尝试了你的代码,虽然它确实让我摆脱了LinqKit的错误,但现在ELinq出现了以下错误。有什么想法吗?这是错误信息:"无法创建类型为'...'的空常量值。在此上下文中,仅支持实体类型、枚举类型或基元类型。" - Josh Mouch
没事了。我觉得这个错误跟你的代码无关。我的表达式开头有一个“(x == null) ? null : ....”,如果我把它移除,就可以工作了。 - Josh Mouch
1
使用您的解决方案后,我遇到了另一个异常:“变量 'p' 的类型为 '(...)' 被引用于作用域 '', 但未定义”。解决方案是跳过某些输入:if (input == null || (input.NodeType == ExpressionType.MemberAccess && !(input.Member is FieldInfo))) return input; - Yankes

10

我创建了一个改进版的 Mic答案

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;

主要优点在于消除具有巨大开销DynamicInvoke,只有在真正需要时才调用Invoke


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接