Объединение выражений в дереве выражений

Как я могу построить дерево выражений, когда части выражения передаются как аргументы?

например. что, если бы я хотел создать деревья выражений, подобные этим:

IQueryable<lxuser> test1(IQueryable<lxuser> query, string foo, string bar)
{
 query=query.Where(x => x.Foo.StartsWith(foo));
 return query.Where(x => x.Bar.StartsWith(bar));
}
</lxuser></lxuser>

но создавая их косвенно:

IQueryable<lxuser> test2(IQueryable<lxuser> query, string foo, string bar)
{
 query=testAdd(query, x => x.Foo, foo);
 return testAdd(query, x => x.Bar, bar);
}
IQueryable<t> testAdd<t>(IQueryable<t> query, 
 Expression<func<t, string="">> select, string find)
{
 // how can I combine the select expression with StartsWith?
 return query.Where(x => select(x) .. y => y.StartsWith(find));
}
</func<t,></t></t></t></lxuser></lxuser>

Результат:

Пока образцы не имели большого смысла (извините, но я старался, чтобы это было просто), вот результат (спасибо Quartermeister).

Он может использоваться с Linq-to-Sql для поиска строки, которая начинается с или равна findText.

public static IQueryable<t> WhereLikeOrExact<t>(IQueryable<t> query, 
 Expression<func<t, string="">> selectField, string findText)
{
 Expression<func<string, bool="">> find;
 if (string.IsNullOrEmpty(findText) || findText=="*") return query;
 if (findText.EndsWith("*")) 
 find=x => x.StartsWith(findText.Substring(0, findText.Length-1));
 else
 find=x => x==findText;
 var p=Expression.Parameter(typeof(T), null);
 var xpr=Expression.Invoke(find, Expression.Invoke(selectField, p));
 return query.Where(Expression.Lambda<func<t, bool="">>(xpr, p));
}
</func<t,></func<string,></func<t,></t></t></t>

например.

var query=context.User;
query=WhereLikeOrExact(query, x => x.FirstName, find.FirstName);
query=WhereLikeOrExact(query, x => x.LastName, find.LastName);
3 ответа

Вы можете использовать Expression.Invoke, чтобы создать выражение, представляющее применение одного выражения к другому, и Expression.Lambda, чтобы создать новое лямбда-выражение для комбинированного выражения. Что-то вроде этого:

IQueryable<t> testAdd<t>(IQueryable<t> query, 
 Expression<func<t, string="">> select, string find)
{
 Expression<func<string, bool="">> startsWith = y => y.StartsWith(find);
 var parameter = Expression.Parameter(typeof(T), null);
 return query.Where(
 Expression.Lambda<func<t, bool="">>(
 Expression.Invoke(
 startsWith,
 Expression.Invoke(select, parameter)),
 parameter));
}
</func<t,></func<string,></func<t,></t></t></t>

Внутреннее выражение Expression.Invoke представляет выражение select(x), а внешнее представляет вызов y => y.StartsWith(find) для значения, возвращаемого select(x).

Вы также можете использовать Expression.Call для представления вызова StartsWith без использования второй лямбда:

IQueryable<t> testAdd<t>(IQueryable<t> query,
 Expression<func<t, string="">> select, string find)
{
 var parameter = Expression.Parameter(typeof(T), null);
 return query.Where(
 Expression.Lambda<func<t, bool="">>(
 Expression.Call(
 Expression.Invoke(select, parameter),
 "StartsWith",
 null,
 Expression.Constant(find)),
 parameter));
}
</func<t,></func<t,></t></t></t>


Это работает:

public IQueryable<t> Add<t>(IQueryable<t> query, Expression<func<t, string="">> Selector1,
 Expression<func<t, string="">> Selector2, string data1, string data2)
{
 return Add(Add(query, Selector1, data1), Selector2, data2);
}
public IQueryable<t> Add<t>(IQueryable<t> query, Expression<func<t, string="">> Selector, string data)
{
 var row = Expression.Parameter(typeof(T), "row");
 var expression =
 Expression.Call(
 Expression.Invoke(Selector, row),
 "StartsWith", null, Expression.Constant(data, typeof(string))
 );
 var lambda = Expression.Lambda<func<t, bool="">>(expression, row);
 return query.Where(lambda);
}
</func<t,></func<t,></t></t></t></func<t,></func<t,></t></t></t>

Вы используете его как:

IQueryable<xluser> query = SomehowInitializeIt();
query = Add(query, x => x.Foo, y => y.Bar, "Foo", "Bar");
</xluser>


Обычно вы не делаете этого так, как вы descirbed (используя интерфейс IQueryable), но вы скорее используете выражения, такие как Expression<func<tresult, t="">></func<tresult,>. Сказав это, вы создаете функции более высокого порядка (например, where или select) в запросе и передаете в выражениях, которые будут "заполнять" желаемую функциональность.

Например, рассмотрим подпись метода Enumerable.Where:

Where<tsource>(IEnumerable<tsource>, Func<tsource, boolean="">)
</tsource,></tsource></tsource>

Функция принимает делегат в качестве второго аргумента, который вызывается для каждого элемента. Значение, которое вы возвращаете из этого делегата, указывает на функцию более высокого порядка, если оно должно дать текущий элемент (включить его в результат или нет).

Теперь взглянем на Queryable.Where:

Queryable.Where<tsource>-Methode (IQueryable<tsource>, Expression<func<tsource, boolean="">>)
</func<tsource,></tsource></tsource>

Мы можем наблюдать тот же шаблон функции более высокого порядка, но вместо делегата Func<> требуется выражение. Выражение в основном представляет собой представление данных вашего кода. Компиляция этого выражения даст вам реальный (исполняемый) делегат. Компилятор делает много тяжелой работы для создания деревьев выражений из lambdas, которые вы назначаете Expression<...>. Деревья выражений позволяют скомпилировать описанный код с различными источниками данных, такими как база данных SQL Server.

Чтобы вернуться к вашему примеру, то, что я думаю, что вы ищете, - это селектор. Селектор принимает каждый элемент ввода и возвращает его проекцию. Его подпись выглядит следующим образом: Expression<func<tresult, t="">></func<tresult,>. Например, вы можете указать это:

Expression<func<int, string="">> numberFormatter = (i) => i.ToString(); // projects an int into a string
</func<int,>

Чтобы перейти в селектор, ваш код должен выглядеть следующим образом:

IQueryable<t> testAdd<t>(IQueryable<t> query, Expression<func<string, t="">> selector, string find)
{
 // how can I combine the select expression with StartsWith?
 return query.Select(selector) // IQueryable<string> now
 .Where(x => x.StartsWith(find));
}
</string></func<string,></t></t></t>

Этот селектор позволит вам проецировать строку ввода на нужный тип. Надеюсь, у меня есть намерение правильно, трудно понять, чего вы пытаетесь достичь.

licensed under cc by-sa 3.0 with attribution.