是否可以从更小的表达式组成 System.Linq.Expressions.Expression?

Is it possible to compose System.Linq.Expressions.Expression from smaller expressions?

我有一个简单的表达式,可用于将域对象转换为 DTO。

public static Expression<Func<Person, PersonDetailsShallow>> ToPersonDetailsShallow
    => (person) => new PersonDetailsShallow()
    {
        PersonId = person.Id,
        Tag = person.Tag
    };

public class Person
{
    public string Id { get; internal set; }
    public string Tag { get; internal set; }
}

public class PersonDetailsShallow
{
    public string PersonId { get; internal set; }
    public string Tag { get; internal set; }
}

我现在梦想有一种方法可以将这个 Expression 嵌入到另一个表达式中,比如

// define one more entity and dto to have something to work with
public class Purchase
{
    public double Price { get; internal set; }
    public Person Purchaser { get; internal set; }
}

public class PurchaseDetailsShallow
{
    public double Price { get; internal set; }
    public PersonDetailsShallow Purchaser { get; internal set; }
}

public static Expression<Func<Purchase, PurchaseDetailsShallow>> ToPurchaseDetailsShallow
    => (purchase) => new PurchaseDetailsShallow()
    {
        Price = purchase.Price,
        Purchaser = ExpandExpression(ToPersonDetailsShallow, purchase.Purchaser)
    }

其中 ExpandExpression 发挥了一些作用,结果 ToPurchaseDetailsShallow 看起来像这样

(purchase) => new PurchaseDetailsShallow()
    {
        Price = purchase.Price,

        Purchaser = new PersonDetailsShallow()
        {
            PersonId = purchase.Purchaser.Id,
            Tag = purchase.Purchaser.Tag
        }
    }

似乎有库可以实现这个问题

但我希望有一种不涉及添加新依赖项的更简单的方法。


我知道我可以使用 Compile() a la

伪造它
public static Expression<Func<Purchase, PurchaseDetailsShallow>> ToPurchaseDetailsShallow
    => (purchase) => new PurchaseDetailsShallow()
    {
        Price = purchase.Price,
        Purchaser = ToPersonDetailsShallow.Compile()(purchase.Purchaser)
    }

然而,它并没有创建正确的表达式树,而只是在计算表达式时表现出类似的行为。

要为 Expression 树实现 lambda 扩展器(还有其他类型的扩展器可以完成),您需要通过使用 XInvoke 方法调用它们来标记要扩展的 lambdas ,创建一个 ExpressionVisitor 来查找调用并展开它们,并使用常见的 Expression 替换访问者将 XInvoke 的参数应用于您正在展开的 lambda。

public static class ExpandXInvokeExt {
    public static TRes XInvoke<TArg1, TRes>(this Expression<Func<TArg1, TRes>> fnE, TArg1 arg1)
        => throw new InvalidOperationException($"Illegal call to XInvoke({fnE},{arg1})");

    public static TRes XInvoke<TArg1, TArg2, TRes>(this Expression<Func<TArg1, TArg2, TRes>> fnE, TArg1 arg1, TArg2 arg2)
        => throw new InvalidOperationException($"Illegal call to XInvoke({fnE},{arg1},{arg2})");

    public static T ExpandXInvoke<T>(this T orig) where T : Expression => (T)new ExpandXInvokeVisitor().Visit(orig);

    public static T Evaluate<T>(this T e) where T : Expression => (T)((e is ConstantExpression c) ? c.Value : Expression.Lambda(e).Compile().DynamicInvoke());

    /// <summary>
    /// ExpressionVisitor to expand a MethodCallExpression of XInvoke with an applied version of the first argument,
    /// an Expression.
    /// </summary>
    public class ExpandXInvokeVisitor : ExpressionVisitor {
        public override Expression Visit(Expression node) {
            if (node?.NodeType == ExpressionType.Call) {
                var callnode = (MethodCallExpression)node;
                if (callnode.Method.Name == "XInvoke" && callnode.Method.DeclaringType == typeof(ExpandXInvokeExt)) {
                    var lambda = (LambdaExpression)(callnode.Arguments[0].Evaluate());
                    Expression expr = lambda.Body;
                    for (int argNum = 1; argNum < callnode.Arguments.Count; ++argNum)
                        expr = expr.Replace(lambda.Parameters[argNum - 1], callnode.Arguments[argNum]);

                    return expr;
                }
            }

            return base.Visit(node);
        }
    }

    /// <summary>
    /// Replaces an Expression (reference Equals) with another Expression
    /// </summary>
    /// <param name="orig">The original Expression.</param>
    /// <param name="from">The from Expression.</param>
    /// <param name="to">The to Expression.</param>
    /// <returns>Expression with all occurrences of from replaced with to</returns>
    public static T Replace<T>(this T orig, Expression from, Expression to) where T : Expression => (T)new ReplaceVisitor(from, to).Visit(orig);

    /// <summary>
    /// ExpressionVisitor to replace an Expression (that is Equals) with another Expression.
    /// </summary>
    public class ReplaceVisitor : ExpressionVisitor {
        readonly Expression from;
        readonly Expression to;

        public ReplaceVisitor(Expression from, Expression to) {
            this.from = from;
            this.to = to;
        }

        public override Expression Visit(Expression node) => node == from ? to : base.Visit(node);
    }
}

这段代码提供了XInvoke一个和两个参数,其他的可以用同样的方式添加。

有了这些可用的扩展,您可以这样写 ToPurchaseDetailsShallow

public static Expression<Func<Purchase, PurchaseDetailsShallow>> ToPurchaseDetailsShallowTemplate
    => (purchase) => new PurchaseDetailsShallow() {
        Price = purchase.Price,
        Purchaser = ToPersonDetailsShallow.Invoke(purchase.Purchaser)
    };

public static Expression<Func<Purchase, PurchaseDetailsShallow>> ToPurchaseDetailsShallow => ToPurchaseDetailsShallowTemplate.ExpandXInvoke();

注意:我使用名称 XInvoke 这样编译器就不会将错误数量的参数与调用 Expression.Invoke 的尝试混淆(我认为不应该,但确实如此)。

注意:如果有足够的括号和转换,您可以避免使用模板变量,但我不确定它是否有更好的效果:

public static Expression<Func<Purchase, PurchaseDetailsShallow>> ToPurchaseDetailsShallow
    => ((Expression<Func<Purchase, PurchaseDetailsShallow>>)(
        (purchase) => new PurchaseDetailsShallow() {
            Price = purchase.Price,
            Purchaser = ToPersonDetailsShallow.XInvoke(purchase.Purchaser)
        })
       )
       .ExpandXInvoke();