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