NHibernate 3 LINQ - как создать действительный параметр для Average()
Скажем, у меня есть очень простая сущность, подобная этой:
public class TestGuy
{
public virtual long Id {get;set;}
public virtual string City {get;set;}
public virtual int InterestingValue {get;set;}
public virtual int OtherValue {get;set;}
}
Этот придуманный пример объекта отображается с помощью NHibernate (с использованием Fluent) и работает нормально.
Время сделать некоторые отчеты. В этом примере "testGuys" является IQueryable с некоторыми уже примененными критериями.
var byCity = testGuys
.GroupBy(c => c.City)
.Select(g => new { City = g.Key, Avg = g.Average(tg => tg.InterestingValue) });
Это работает просто отлично. В NHibernate Profiler я вижу, что генерируется правильный SQL, и результаты соответствуют ожидаемым.
Вдохновленный моим успехом, я хочу сделать его более гибким. Я хочу сделать его настраиваемым, чтобы пользователь мог получить среднее значение OtherValue, а также InterestingValue. Не должно быть слишком сложно, аргумент Average() выглядит как Func (так как значения в данном случае являются целочисленными). Очень просто. Разве я не могу просто создать метод, который возвращает Func на основе некоторого условия, и использовать его в качестве аргумента?
var fieldToAverageBy = GetAverageField(SomeEnum.Other);
private Func<TestGuy,int> GetAverageField(SomeEnum someCondition)
{
switch(someCondition)
{
case SomeEnum.Interesting:
return tg => tg.InterestingValue;
case SomeEnum.Other:
return tg => tg.OtherValue;
}
throw new InvalidOperationException("Not in my example!");
}
И тогда, в другом месте, я мог бы просто сделать это:
var byCity = testGuys
.GroupBy(c => c.City)
.Select(g => new { City = g.Key, Avg = g.Average(fieldToAverageBy) });
Ну, я думал, что смогу сделать это. Однако, когда я перечисляю это, NHibernate подбрасывает:
Object of type 'System.Linq.Expressions.ConstantExpression' cannot be converted to type 'System.Linq.Expressions.LambdaExpression'.
Так что я предполагаю, что за кулисами происходит какое-то преобразование или преобразование или что-то подобное, что в первом случае принимает мою лямбду, но во втором случае превращается в нечто, что NHibernate не может преобразовать в SQL.
Надеюсь, мой вопрос прост - как моя функция GetAverageField может возвращать что-то, что будет работать в качестве параметра для Average(), если поддержка NHibernate 3.0 LINQ (метод.Query()) переводит это в SQL?
Любые предложения приветствуются, спасибо!
РЕДАКТИРОВАТЬ
Основываясь на комментариях Дэвида Б. в его ответе, я внимательно посмотрел на это. Мое предположение, что Func будет правильным возвращаемым типом, было основано на значении intellisense, которое я получил для метода Average(). Кажется, он основан на типе Enumerable, а не на Queryable. Это странно.. Нужно присмотреться к вещам.
Метод GroupBy имеет следующую возвращаемую подпись:
IQueryable<IGrouping<string,TestGuy>>
Это означает, что это должно дать мне IQueryable, хорошо. Однако затем я перехожу к следующей строке:
.Select(g => new { City = g.Key, Avg = g.Average(tg => tg.InterestingValue) });
Если я проверяю intellisense для данной переменной внутри нового определения объекта { }, он фактически отображается как тип IGrouping - NOT IQueryable>. Вот почему вызванный метод Average() является Enumerable и почему он не принимает параметр Expression, предложенный David B.
Так что каким-то образом моя групповая ценность где-то потеряла свой статус IQueryable.
Немного интересная заметка:
Я могу изменить выбор на следующее:
.Select(g => new { City = g.Key, Avg = g.AsQueryable<TestGuy>().Average(fieldToAverageBy) });
И теперь он компилируется! Черная магия! Однако это не решает проблему, так как NHibernate больше не любит меня и дает следующее исключение:
Could not parse expression '[-1].AsQueryable()': This overload of the method 'System.Linq.Queryable.AsQueryable' is currently not supported, but you can register your own parser if needed.
Меня сбивает с толку то, что это работает, когда я передаю лямбда-выражение методу Average(), но я не могу найти простой способ представления того же выражения в качестве аргумента. Я явно делаю что-то не так, но не вижу, что...!?
Я в своем уме. Помоги мне, Джон Скит, ты моя единственная надежда!;)
2 ответа
Вы не сможете вызывать "локальный" метод в своем лямбда-выражении. Если бы это было простое не вложенное предложение, оно было бы относительно простым - вам просто нужно изменить это:
private Func<TestGuy,int> GetAverageField(SomeEnum someCondition)
к этому:
private Expression<Func<TestGuy,int>> GetAverageField(SomeEnum someCondition)
а затем передать результат вызова в соответствующий метод запроса, например,
var results = query.Select(GetAverageField(fieldToAverageBy));
В этом случае, однако, вам нужно будет построить все дерево выражений для Select
предложение - выражение создания анонимного типа, извлечение ключа и извлечение средней части поля. Это не будет весело, если честно. В частности, к тому времени, как вы построите свое дерево выражений, оно не будет статически типизировано так же, как обычное выражение запроса, из-за невозможности выразить анонимный тип в объявлении.
Если вы используете.NET 4, динамическая типизация может вам помочь, хотя вы, конечно, заплатите цену за то, что больше не будете использовать статическую типизацию.
Один из вариантов (хотя это может быть ужасно) - попытаться использовать своего рода "шаблон" дерева выражений проекции анонимного типа (например, всегда используя одно свойство), а затем создать копию этого дерева выражений, вставив правильное выражение. вместо. Опять же, это не будет весело.
Марк Грэвелл, возможно, сможет помочь в этом - это действительно похоже на то, что должно быть возможно, но я не знаю, как сделать это элегантно в данный момент.
А? the parameter to Queryable.Average is not Func<T, U>
, Это Expression<Func<T, U>>
Способ сделать это:
private Expression<Func<TestGuy,int>> GetAverageExpr(SomeEnum someCondition)
{
switch(someCondition)
{
case SomeEnum.Interesting:
return tg => tg.InterestingValue;
case SomeEnum.Other:
return tg => tg.OtherValue;
}
throw new InvalidOperationException("Not in my example!");
}
С последующим:
Expression<Func<TestGuy, int>> averageExpr = GetAverageExpr(someCondition);
var byCity = testGuys
.GroupBy(c => c.City)
.Select(g => new { City = g.Key, Avg = g.Average(averageExpr) });