如何在不同(但兼容)模型之间转换lambda表达式?

18

(基于一封电子邮件对话,现在已记录下来以供信息共享)我有两个用于不同层次的模型:

public class TestDTO {
    public int CustomerID { get; set; }
}
//...
public class Test {
    public int CustomerID { get; set; }
}

并且在我的DTO层中,lambda表达式如下:

Expression<Func<TestDTO, bool>> fc1 =
   (TestDTO c1) => c1.CustomerID <= 100 && c1.CustomerID >= 10;
我怎样才能将那个 lambda(在一般情况下)转化为讨论另一个模型的 lambda?
Expression<Func<Test, bool>> fc2 = {insert magic here, based on fc1}

显然,我们在寻求相同的测试条件,但是使用Test类型。

2个回答

20
为了做到这一点,您需要完全重建表达式树;参数需要重新映射,并且所有现在与不同类型交互的成员访问都需要重新应用。幸运的是,ExpressionVisitor类可以使这些工作更加容易;例如(在一般情况下完成所有工作,而不仅仅是Func<T,bool>谓词使用):
class TypeConversionVisitor : ExpressionVisitor
{
    private readonly Dictionary<Expression, Expression> parameterMap;

    public TypeConversionVisitor(
        Dictionary<Expression, Expression> parameterMap)
    {
        this.parameterMap = parameterMap;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        // re-map the parameter
        Expression found;
        if(!parameterMap.TryGetValue(node, out found))
            found = base.VisitParameter(node);
        return found;
    }
    protected override Expression VisitMember(MemberExpression node)
    {
        // re-perform any member-binding
        var expr = Visit(node.Expression);
        if (expr.Type != node.Type)
        {
            MemberInfo newMember = expr.Type.GetMember(node.Member.Name)
                                       .Single();
            return Expression.MakeMemberAccess(expr, newMember);
        }
        return base.VisitMember(node);
    }
}

在这里,我们传入一个参数字典来重新映射,并将其应用于VisitParameter中。此外,在VisitMember中,我们还检查是否已经切换了类型(如果Visit涉及ParameterExpression或另一个MemberExpression,则可以在任何时候发生):如果是,则尝试找到同名的另一个成员。
接下来,我们需要一个通用的lambda转换重写方法:
// allows extension to other signatures later...
private static Expression<TTo> ConvertImpl<TFrom, TTo>(Expression<TFrom> from)
    where TFrom : class
    where TTo : class
{
    // figure out which types are different in the function-signature
    var fromTypes = from.Type.GetGenericArguments();
    var toTypes = typeof(TTo).GetGenericArguments();
    if (fromTypes.Length != toTypes.Length)
        throw new NotSupportedException(
            "Incompatible lambda function-type signatures");
    Dictionary<Type, Type> typeMap = new Dictionary<Type,Type>();
    for (int i = 0; i < fromTypes.Length; i++)
    {
        if (fromTypes[i] != toTypes[i])
            typeMap[fromTypes[i]] = toTypes[i];
    }

    // re-map all parameters that involve different types
    Dictionary<Expression, Expression> parameterMap
        = new Dictionary<Expression, Expression>();
    ParameterExpression[] newParams =
        new ParameterExpression[from.Parameters.Count];
    for (int i = 0; i < newParams.Length; i++)
    {
        Type newType;
        if(typeMap.TryGetValue(from.Parameters[i].Type, out newType))
        {
            parameterMap[from.Parameters[i]] = newParams[i] =
                Expression.Parameter(newType, from.Parameters[i].Name);
        }
        else
        {
            newParams[i] = from.Parameters[i];
        }
    }

    // rebuild the lambda
    var body = new TypeConversionVisitor(parameterMap).Visit(from.Body);
    return Expression.Lambda<TTo>(body, newParams);
}

这个方法接受一个任意的Expression<TFrom>和一个TTo,通过以下步骤将其转换为Expression<TTo>
  • 找出TFrom / TTo之间不同的类型
  • 使用它来重新映射参数
  • 使用我们刚刚创建的表达式访问器
  • 最后构造所需签名的新lambda表达式
然后,将所有内容组合起来并公开我们的扩展方法。
public static class Helpers {
    public static Expression<Func<TTo, bool>> Convert<TFrom, TTo>(
        this Expression<Func<TFrom, bool>> from)
    {
        return ConvertImpl<Func<TFrom, bool>, Func<TTo, bool>>(from);
    }

    // insert from above: ConvertImpl
    // insert from above: TypeConversionVisitor
}

et voila; 一个通用的 lambda 转换程序,具有特定实现:

Expression<Func<Test, bool>> fc2 = fc1.Convert<TestDTO, Test>();

@Kin ta:我忘记回来看了,接受自己的答案会有一些延迟。 - Marc Gravell
@MarcGravell,我在使用您发布的代码时遇到了问题。通常情况下您的代码都非常好用,但是如果目标类型中不存在源类型和表达式中存在的成员怎么办呢?我只想忽略那个表达式,但是我找不到方法。请问您能帮我吗? - bahadir arslan
@bahadirarslan 你不能检查一下然后跳过它吗?不清楚这个间隙在哪里... - Marc Gravell
@MarcGravell 在代码的这一行出现了错误:MemberInfo newMember = expr.Type.GetMember(node.Member.Name).Single(); 是的,我也这么想,但我无法跳过它。返回null、Expression.Default或其他任何东西都不起作用。因为expr.Type.GetMember(node.Member.Name)里面没有任何内容。 - bahadir arslan
太好了,我刚刚写了一个非常长的问题,就是要找这个。由于现在已经有了它,所以我把问题删掉了。谢谢。 - Jcl

5
您可以使用AutoMapper(不需要表达式树):

你可以使用AutoMapper(无需表达式树):

Mapper.CreateMap<Test, TestDTO>();

...

Func<TestDTO, bool> fc1 =
  (TestDTO c1) => c1.CustomerID <= 100 && c1.CustomerID >= 10;

Func<Test, bool> fc2 =
  (Test t) => fc1(Mapper.Map<Test, TestDTO>(t));

2
这对于LINQ-to-Objects是可行的,但它不允许与基于Expression<T> API的ORM和其他工具一起使用。虽然值得一提,所以肯定是+1;但我认为它并没有在预期的情况下有所帮助。 - Marc Gravell

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