【发布时间】:2016-10-05 01:49:45
【问题描述】:
我希望能够使用嵌套扩展方法将 EF 中的实体投影到相应的视图模型。 (有关我在做什么的更多详细信息,请参阅我之前的问题Projection of single entities in EF with extension methods)。
根据这个问题,我构建了一个属性来用 lambda 替换表达式树中的扩展方法,以便能够做到这一点。它从扩展方法中获取方法参数,并在调用 VisitParameter 时替换它们(我不知道是否有办法替换 LambdaExpression 中的内联参数)。
这很适合这样的事情:
entity => new ProfileModel
{
Name = entity.Name
}
我可以看到表达式访问者将 LambdaExpression 上的实体参数替换为扩展方法 args 中的正确参数。
但是,当我将其更改为更嵌套的内容时,
entity => new ProfileModel
{
SomethingElses = entity.SomethingElses.AsQueryable().ToViewModels()
}
然后我得到:
参数“entity”未绑定在指定的 LINQ to Entities 查询表达式中。
另外,VisitParameter 在我的表达式中访问者似乎根本没有被参数“实体”调用。
就像它根本不使用我的访问者来处理第二个 Lambda,但我不知道为什么它会用于一个而不是另一个?
在两种 lambda 表达式的情况下如何正确替换参数?
下面是我的访客:
protected override Expression VisitMethodCall(MethodCallExpression node)
{
bool expandNode = node.Method.GetCustomAttributes(typeof(ExpandableMethodAttribute), false).Any();
if (expandNode && node.Method.IsStatic)
{
object[] args = new object[node.Arguments.Count];
args[0] = _provider.CreateQuery(node.Arguments[0]);
for (int i = 1; i < node.Arguments.Count; i++)
{
Expression arg = node.Arguments[i];
args[i] = (arg.NodeType == ExpressionType.Constant) ? ((ConstantExpression)arg).Value : arg;
}
return ((IQueryable)node.Method.Invoke(null, args)).Expression;
}
var replaceNodeAttributes = node.Method.GetCustomAttributes(typeof(ReplaceInExpressionTree), false).Cast<ReplaceInExpressionTree>();
if (replaceNodeAttributes.Any() && node.Method.IsStatic)
{
var replaceWith = node.Method.DeclaringType.GetMethod(replaceNodeAttributes.First().MethodName).Invoke(null, null);
if (replaceWith is LambdaExpression)
{
RegisterReplacementParameters(node.Arguments.ToArray(), replaceWith as LambdaExpression);
return Visit((replaceWith as LambdaExpression).Body);
}
}
return base.VisitMethodCall(node);
}
protected override Expression VisitParameter(ParameterExpression node)
{
Expression replacement;
if (_replacements.TryGetValue(node, out replacement))
return Visit(replacement);
return base.VisitParameter(node);
}
private void RegisterReplacementParameters(Expression[] parameterValues, LambdaExpression expressionToVisit)
{
if (parameterValues.Length != expressionToVisit.Parameters.Count)
throw new ArgumentException(string.Format("The parameter values count ({0}) does not match the expression parameter count ({1})", parameterValues.Length, expressionToVisit.Parameters.Count));
foreach (var x in expressionToVisit.Parameters.Select((p, idx) => new { Index = idx, Parameter = p }))
{
if (_replacements.ContainsKey(x.Parameter))
{
throw new Exception("Parameter already registered, this shouldn't happen.");
}
_replacements.Add(x.Parameter, parameterValues[x.Index]);
}
}
此处的完整重现代码示例:https://github.com/lukemcgregor/ExtensionMethodProjection
编辑:
我现在有一篇博文 (Composable Repositories - Nesting Extensions) 和 nuget package 来帮助在 linq 中嵌套扩展方法
【问题讨论】:
-
因为
ToViewModels将拥有expandNodetrue,导致您返回一个将被遍历的新表达式,基本上丢弃了您的原始表达式。请记住,您将解析 ToViewModels -> AsQueryable -> SomethingElses -> 实体。 -
@rob 不确定您所说的 expandNode 是什么意思?那是什么,它是如何工作的?我可以绕过这个并继续使用这个表达式吗?
-
@downvoter 愿意评论一下您为什么认为这是一个糟糕的问题?
-
在
VisitMethodCall中,您有一个基于属性的标志expandNode(ToViewModels标记有)。当expandNode设置为true(和静态)时,它最终返回((IQueryable)node.Method.Invoke(null, args)).Expression。这是一个可能不包含原始参数表达式的新表达式。我无法运行您的完整代码(在工作中),但乍一看,这很可能是您在第二个示例中没有看到VisitParameter执行的原因 -
哦,对了,对不起,我以为你在谈论一些表达式树争论。那是因为 Invoke 会创建一个全新的表达式吗?我将它包裹在
Visit()中,现在一切正常,谢谢。
标签: c# lambda expression-trees