我希望能够使用嵌套扩展方法将EF中的实体投影到相应的视图模型。 (有关我正在做什么的详细信息,请参阅我之前的问题Projection of single entities in EF with extension methods。
根据这个问题,我构建了一个属性,用一个lambda替换表达式树中的扩展方法,以便能够做到这一点。它接受来自extentsion方法的方法参数,并在调用VisitParameter时替换它们(我不知道是否有办法在LambdaExpression中替换内联参数)。
这适用于这样的事情:
entity => new ProfileModel
{
Name = entity.Name
}
我可以看到表达式visitor将LambdaExpression上的entity参数替换为扩展方法args中的正确参数。
但是,当我将其更改为更嵌套的内容时,
entity => new ProfileModel
{
SomethingElses = entity.SomethingElses.AsQueryable().ToViewModels()
}
然后我得到:
参数'实体'未绑定在指定的LINQ to Entities查询表达式中。
另外VisitParameter
在我的表达式访问者中似乎没有使用参数' entity'来调用。
它就像它根本没有使用我的访客第二个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中嵌套扩展方法
答案 0 :(得分:1)
首先要记住的是,在解析节点时,我们基本上是向后运行:
entity => new ProfileModel
{
SomethingElses = entity.SomethingElses.AsQueryable().ToViewModels()
}
在此处,我们处理ToViewModels()
,然后AsQueryable()
,然后SomethingElses
,最后entity
。由于我们发现entity
永远不会被解析(VisitParameter
),这意味着我们的链中的某些东西停止了树的遍历。
我们有两个罪魁祸首:
VisitMethodCall()
(AsQueryable和ToViewModels)和VisitMemberAccess()
(SomethingElses)
我们未覆盖VisitMemberAccess
,因此问题必须在VisitMethodCall
我们有三个该方法的退出点:
return ((IQueryable)node.Method.Invoke(null, args)).Expression;
return Visit((replaceWith as LambdaExpression).Body);
return base.VisitMethodCall(node);
第一行逐字返回表达式,并停止进一步遍历树。这意味着永远不会访问后代节点 - 因为我们说这项工作基本上完成了。这是否是正确的行为实际上取决于您希望与访问者达成的目标。
将代码更改为
return Visit(((IQueryable)node.Method.Invoke(null, args)).Expression);
意味着我们遍历这个(可能是新的!)表达式。这并不能保证我们能够访问正确的节点(例如,这个表达式可能完全独立于原始节点) - 但 意味着如果这个新表达式包含一个参数表达式,可以正确访问参数表达式。
答案 1 :(得分:0)
我认为你过于复杂了。见访客:
public class CustomerVM { }
public class Customer {}
public class ReplaceMethodAttribute: Attribute
{
public string ReplacementMethodName {get; private set;}
public ReplaceMethodAttribute(string name)
{
ReplacementMethodName = name;
}
}
public static class Extensions
{
public static CustomerVM ToCustomerVM(Customer customer)
{
throw new NotImplementedException();
}
[ReplaceMethod("Extensions.ToCustomerVM")]
public static CustomerVM ToVM(this Customer customer)
{
return Extensions.ToCustomerVM(customer);
}
}
public class ReplaceMethodVisitor: ExpressionVisitor
{
protected override Expression VisitMethodCall(MethodCallExpression exp)
{
var attr = exp.Method.GetCustomAttributes(typeof(ReplaceMethodAttribute), true).OfType<ReplaceMethodAttribute>().FirstOrDefault();
if (attr != null)
{
var parameterTypes = exp.Method.GetParameters().Select(i => i.ParameterType).ToArray();
var mi = GetMethodInfo(attr.ReplacementMethodName, parameterTypes);
return Visit(Expression.Call(mi, exp.Arguments));
}
return base.VisitMethodCall(exp);
}
private MethodInfo GetMethodInfo(string name, Type[] argumentTypes)
{
// enhance with input checking
var lastDot = name.LastIndexOf('.');
var type = name.Substring(0, lastDot);
var methodName = name.Substring(lastDot);
return this.GetType().Assembly.GetTypes().Single(x => x.FullName == type).GetMethod(methodName, argumentTypes); // this might need adjusting if types are in different assembly
}
}