Home > front end >  Is it possible to compose System.Linq.Expressions.Expression from smaller expressions?
Is it possible to compose System.Linq.Expressions.Expression from smaller expressions?

Time:03-18

I have a simple expression that I use to convert domain objects into DTOs.

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; }
}

I am now dreaming of a way to embed this Expression into another expression, something like

// 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)
    }

where ExpandExpression works some magic such that the resulting ToPurchaseDetailsShallow looks like this

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

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

It appears that there are libraries that can achieve this as shown in this question

Can I reuse code for selecting a custom DTO object for a child property with EF Core?

but I am hoping for a simpler way that does not involve adding new dependencies.


I am aware that I could fake it using Compile() a la

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

which however does not create the right expression tree, but only shows similar behavior when evaluating the expression.

CodePudding user response:

To implement a lambda expander for an Expression tree (there are other types of expanders that can be done), you need to mark the lambdas to expand by calling them using the XInvoke method, create an ExpressionVisitor to find the calls and expand them, and use the common Expression replace visitor to apply the arguments of XInvoke to the lambda you are expanding.

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);
    }
}

This code provides XInvoke for one and two arguments, others can be added in the same way.

With these extensions available, you can write your ToPurchaseDetailsShallow like this:

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();

Note: I used the name XInvoke so the compiler wouldn't confuse incorrect number of arguments with attempts to call Expression.Invoke (I don't think it should, but it does).

Note: With enough parentheses and casting, you can avoid the template variable, but I am not sure it makes anything better:

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