0

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.

Benj
  • 889
  • 1
  • 14
  • 31
  • Generally speaking, expression lambdas are not particularly composable (there are simple cases you can do that a bit) without rolling up your sleaves and using the `Expression` factory methods to construct your tree by hand. Learning how to do that is a bit of work, but once you get used to it, it's not unrealistic for you to be able to pull off any bit of composability you want. – Kirk Woll Mar 17 '22 at 14:24
  • Use `LINQKit` as I mentioned. If you do not have knowledge how to work with Expression Tree - it is wasting of time. Actually it is my implementation of `ExpandableAttribute` in `LINQKit`. If you do not want additional dependencies - just copy/paste code from `LINQKit`. Root is [ExpressionExpander.cs](https://github.com/scottksmith95/LINQKit/blob/master/src/LinqKit.Core/ExpressionExpander.cs) – Svyatoslav Danyliv Mar 17 '22 at 15:02
  • Note that `ToPurchaseDetailsShallow` is an `Expression<>` so when you embed `ExpandExpression` in it the compiler won't create executable code to run it - you have to do that outside the `Expression<>`. – NetMage Mar 17 '22 at 15:23

1 Answers1

2

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();
NetMage
  • 26,163
  • 3
  • 34
  • 55
  • Just out of curiosity what are the other types of Expanders you had in mind? – Benj Mar 19 '22 at 07:48
  • 1
    @Benj Here are a couple: 1 uses attributes to replace a method call with another `Expression` by calling another method e.g. replaces `f.RemoveAll("x..z")` with `f.Replace("x", "")...Replace("z", "")` by calling a `RemoveAllExpander` method during the visit 2. One that takes an `Expression` that contains `expressionvariable.Embed()` and replaces the method call to `Embed` with the value of the `expressionvariable` e.g. `var inner2 = () => 4*8; Expression> wrap2 = x => x == innter2.Body.Embed(); var exp = wrap2.ExpandEmbed();` yields `exp` with value `x => x == 32`. – NetMage Mar 21 '22 at 19:54