У меня есть простое выражение, которое я использую для преобразования объектов домена в 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() а-ля
public static Expression<Func<Purchase, PurchaseDetailsShallow>> ToPurchaseDetailsShallow
=> (purchase) => new PurchaseDetailsShallow()
{
Price = purchase.Price,
Purchaser = ToPersonDetailsShallow.Compile()(purchase.Purchaser)
}
который, однако, не создает правильное дерево выражения, а только показывает аналогичное поведение при оценке выражения.
Используйте LINQKit, как я уже говорил. Если вы не умеете работать с Expression Tree - это пустая трата времени. На самом деле это моя реализация ExpandableAttribute в LINQKit. Если вы не хотите дополнительных зависимостей - просто скопируйте/вставьте код из LINQKit. Корень ExpressionExpander.cs
Обратите внимание, что ToPurchaseDetailsShallow — это Expression<>, поэтому, когда вы встраиваете ExpandExpression в него, компилятор не будет создавать исполняемый код для его запуска — вы должны делать это вне Expression<>.





Чтобы реализовать лямбда-расширитель для Expression-дерева (есть и другие типы экспандеров, которые можно сделать), вам нужно пометить лямбды для расширения, вызвав их с помощью метода XInvoke, создать ExpressionVisitor для поиска вызовов и их расширения, и используйте общий посетитель замены Expression, чтобы применить аргументы XInvoke к лямбде, которую вы расширяете.
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();
Просто из любопытства, какие другие типы расширителей вы имели в виду?
@Benj Вот пара: 1 использует атрибуты для замены вызова метода другим Expression, вызывая другой метод, например. заменяет f.RemoveAll("x..z") на f.Replace("x", "")...Replace("z", ""), вызывая метод RemoveAllExpander во время посещения 2. Тот, который принимает Expression, содержащий expressionvariable.Embed<T>(), и заменяет вызов метода Embed значением expressionvariable, например. var inner2 = () => 4*8; Expression<Func<int,bool>> wrap2 = x => x == innter2.Body.Embed<int>(); var exp = wrap2.ExpandEmbed(); дает exp значение x => x == 32.
Вообще говоря, лямбда-выражения не особенно поддаются компоновке (есть простые случаи, которые вы можете сделать немного), не сворачивая рукава и не используя
Expressionфабричные методы для построения дерева вручную. Изучение того, как это сделать, требует некоторой работы, но как только вы привыкнете к этому, вы сможете добиться любой части компонуемости, которую захотите.