#c# #linq #linq-expressions
#c# #linq #linq-выражения
Вопрос:
Как объединить несколько похожих выражений выбора в одно выражение?
private static Expression<Func<Agency, AgencyDTO>> CombineSelectors(params Expression<Func<Agency, AgencyDTO>>[] selectors)
{
// ???
return null;
}
private void Query()
{
Expression<Func<Agency, AgencyDTO>> selector1 = x => new AgencyDTO { Name = x.Name };
Expression<Func<Agency, AgencyDTO>> selector2 = x => new AgencyDTO { Phone = x.PhoneNumber };
Expression<Func<Agency, AgencyDTO>> selector3 = x => new AgencyDTO { Location = x.Locality.Name };
Expression<Func<Agency, AgencyDTO>> selector4 = x => new AgencyDTO { EmployeeCount = x.Employees.Count() };
using (RealtyContext context = Session.CreateContext())
{
IQueryable<AgencyDTO> agencies = context.Agencies.Select(CombineSelectors(selector3, selector4));
foreach (AgencyDTO agencyDTO in agencies)
{
// do something..;
}
}
}
Комментарии:
1. Показать данные в списке. Это необходимо для того, чтобы избежать загрузки ненужных полей из базы данных.
Ответ №1:
Не просто; вам нужно переписать все выражения — ну, строго говоря, вы можете переработать большую часть одного из них, но проблема в том, что в каждом из них они разные x
(даже если они выглядят одинаково), следовательно, вам нужно использовать visitor для замены всех параметров на final x
. К счастью, в 4.0 это не так уж плохо:
static void Main() {
Expression<Func<Agency, AgencyDTO>> selector1 = x => new AgencyDTO { Name = x.Name };
Expression<Func<Agency, AgencyDTO>> selector2 = x => new AgencyDTO { Phone = x.PhoneNumber };
Expression<Func<Agency, AgencyDTO>> selector3 = x => new AgencyDTO { Location = x.Locality.Name };
Expression<Func<Agency, AgencyDTO>> selector4 = x => new AgencyDTO { EmployeeCount = x.Employees.Count() };
// combine the assignments from the 4 selectors
var convert = Combine(selector1, selector2, selector3, selector4);
// sample data
var orig = new Agency
{
Name = "a",
PhoneNumber = "b",
Locality = new Location { Name = "c" },
Employees = new List<Employee> { new Employee(), new Employee() }
};
// check it
var dto = new[] { orig }.AsQueryable().Select(convert).Single();
Console.WriteLine(dto.Name); // a
Console.WriteLine(dto.Phone); // b
Console.WriteLine(dto.Location); // c
Console.WriteLine(dto.EmployeeCount); // 2
}
static Expression<Func<TSource, TDestination>> Combine<TSource, TDestination>(
params Expression<Func<TSource, TDestination>>[] selectors)
{
var zeroth = ((MemberInitExpression)selectors[0].Body);
var param = selectors[0].Parameters[0];
List<MemberBinding> bindings = new List<MemberBinding>(zeroth.Bindings.OfType<MemberAssignment>());
for (int i = 1; i < selectors.Length; i )
{
var memberInit = (MemberInitExpression)selectors[i].Body;
var replace = new ParameterReplaceVisitor(selectors[i].Parameters[0], param);
foreach (var binding in memberInit.Bindings.OfType<MemberAssignment>())
{
bindings.Add(Expression.Bind(binding.Member,
replace.VisitAndConvert(binding.Expression, "Combine")));
}
}
return Expression.Lambda<Func<TSource, TDestination>>(
Expression.MemberInit(zeroth.NewExpression, bindings), param);
}
class ParameterReplaceVisitor : ExpressionVisitor
{
private readonly ParameterExpression from, to;
public ParameterReplaceVisitor(ParameterExpression from, ParameterExpression to)
{
this.from = from;
this.to = to;
}
protected override Expression VisitParameter(ParameterExpression node)
{
return node == from ? to : base.VisitParameter(node);
}
}
При этом используется конструктор из первого найденного выражения, поэтому вы, возможно, захотите проверить работоспособность, чтобы все остальные использовали тривиальные конструкторы в своих соответствующих NewExpression
файлах. Впрочем, я оставил это для читателя.
Редактировать: В комментариях @Slaks отмечает, что больше LINQ могло бы сделать это короче. Он, конечно, прав — немного запутан для удобства чтения, хотя:
static Expression<Func<TSource, TDestination>> Combine<TSource, TDestination>(
params Expression<Func<TSource, TDestination>>[] selectors)
{
var param = Expression.Parameter(typeof(TSource), "x");
return Expression.Lambda<Func<TSource, TDestination>>(
Expression.MemberInit(
Expression.New(typeof(TDestination).GetConstructor(Type.EmptyTypes)),
from selector in selectors
let replace = new ParameterReplaceVisitor(
selector.Parameters[0], param)
from binding in ((MemberInitExpression)selector.Body).Bindings
.OfType<MemberAssignment>()
select Expression.Bind(binding.Member,
replace.VisitAndConvert(binding.Expression, "Combine")))
, param);
}
Комментарии:
1. 1 за написание всего этого кода. Это можно было бы сделать намного проще, заменив вложенный цикл вызовами LINQ и новым
Parameter
2. @Slaks может быть, может быть. Хотя это было уже достаточно сложно — у читателя, вероятно, больше шансов использовать его в более процедурном формате
3. @Slaks — добавление, просто для удовольствия
4. Я адаптировал ваш код, и он отлично работает.. Но я бы хотел работать с выражениями типа Expression<Func<Агентство, объект>> selector1 = x => new { Name = x.Name }; вместо выражения<Func<Agency, AgencyDTO>> selector1 = x => new AgencyDTO { Name = x.Name }; Как я могу заставить его работать с такими анонимными типами? Это выражение (MemberInitExpression) селектор. Тело прерывается, если используются анонимные типы
Ответ №2:
Если все селекторы будут только инициализировать AgencyDTO
объекты (как в вашем примере), вы можете преобразовать выражения в NewExpression
экземпляры, а затем вызвать Expression.New
с Members
помощью выражений.
Вам также понадобится ExpressionVisitor
заменить ParameterExpression
символы из исходных выражений на один ParameterExpression
для выражения, которое вы создаете.
Комментарии:
1. На самом деле это
MemberInitExpression
(NewExpression
это просто ctor); это можно сделать, хотя (добавлено)
Ответ №3:
На случай, если кто-нибудь еще наткнется на это с аналогичным вариантом использования, как у меня (мой выбор нацелен на разные классы в зависимости от необходимого уровня детализации):
Упрощенный сценарий:
public class BlogSummaryViewModel
{
public string Name { get; set; }
public static Expression<Func<Data.Blog, BlogSummaryViewModel>> Map()
{
return (i => new BlogSummaryViewModel
{
Name = i.Name
});
}
}
public class BlogViewModel : BlogSummaryViewModel
{
public int PostCount { get; set; }
public static Expression<Func<Data.Blog, BlogViewModel>> Map()
{
return (i => new BlogViewModel
{
Name = i.Name,
PostCount = i.Posts.Count()
});
}
}
Я адаптировал решение, предоставленное @Marc Gravell, следующим образом:
public static class ExpressionMapExtensions
{
public static Expression<Func<TSource, TTargetB>> Concat<TSource, TTargetA, TTargetB>(
this Expression<Func<TSource, TTargetA>> mapA, Expression<Func<TSource, TTargetB>> mapB)
where TTargetB : TTargetA
{
var param = Expression.Parameter(typeof(TSource), "i");
return Expression.Lambda<Func<TSource, TTargetB>>(
Expression.MemberInit(
((MemberInitExpression)mapB.Body).NewExpression,
(new LambdaExpression[] { mapA, mapB }).SelectMany(e =>
{
var bindings = ((MemberInitExpression)e.Body).Bindings.OfType<MemberAssignment>();
return bindings.Select(b =>
{
var paramReplacedExp = new ParameterReplaceVisitor(e.Parameters[0], param).VisitAndConvert(b.Expression, "Combine");
return Expression.Bind(b.Member, paramReplacedExp);
});
})),
param);
}
private class ParameterReplaceVisitor : ExpressionVisitor
{
private readonly ParameterExpression original;
private readonly ParameterExpression updated;
public ParameterReplaceVisitor(ParameterExpression original, ParameterExpression updated)
{
this.original = original;
this.updated = updated;
}
protected override Expression VisitParameter(ParameterExpression node) => node == original ? updated : base.VisitParameter(node);
}
}
Map
Метод расширенного класса затем становится:
public static Expression<Func<Data.Blog, BlogViewModel>> Map()
{
return BlogSummaryViewModel.Map().Concat(i => new BlogViewModel
{
PostCount = i.Posts.Count()
});
}