4

假设我有一个非常简单的实体,如下所示:

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'.

所以我猜测在幕后,一些转换或强制转换或一些这样的事情正在发生,在第一种情况下接受我的 lambda,但在第二种情况下,NHibernate 无法转换为 SQL。

My question is hopefully simple - how can my GetAverageField function return something that will work as a parameter to Average() when NHibernate 3.0 LINQ support (the .Query() method) translates this to SQL?

Any suggestions welcome, thanks!

EDIT

Based on the comments from David B in his answer, I took a closer look at this. My assumption that Func would be the right return type was based on the intellisense I got for the Average() method. It seems to be based on the Enumerable type, not the Queryable one. That's strange.. Need to look a bit closer at stuff.

The GroupBy method has the following return signature:

IQueryable<IGrouping<string,TestGuy>>

That means it should give me an IQueryable, all right. However, I then move on to the next line:

.Select(g => new { City = g.Key, Avg = g.Average(tg => tg.InterestingValue) });

If I check the intellisense for the g variable inside the new { } object definition, it is actually listed as being of type IGrouping - NOT IQueryable>. This is why the Average() method called is the Enumerable one, and why it won't accept the Expression parameter suggested by David B.

So somehow my group value has apparently lost it's status as an IQueryable somewhere.

Slightly interesting note:

I can change the Select to the following:

.Select(g => new { City = g.Key, Avg = g.AsQueryable<TestGuy>().Average(fieldToAverageBy) });

And now it compiles! Black magic! However, that doesn't solve the issue, as NHibernate now doesn't love me anymore and gives the following exception:

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.

What baffles me is that this works when I give the lambda expression to the Average() method, but that I can't find a simple way to represent the same expression as an argument. I am obviously doing something wrong, but can't see what...!?

I am at my wits end. Help me, Jon Skeet, you're my only hope! ;)

4

2 回答 2

2

You won't be able to call a "local" method within your lambda expression. If this were a simple non-nested clause, it would be relatively simple - you'd just need to change this:

private Func<TestGuy,int> GetAverageField(SomeEnum someCondition)

to this:

private Expression<Func<TestGuy,int>> GetAverageField(SomeEnum someCondition)

and then pass the result of the call into the relevant query method, e.g.

var results = query.Select(GetAverageField(fieldToAverageBy));

In this case, however, you'll need to build the whole expression tree up for the Select clause - the anonymous type creation expression, the extraction of the Key, and the extraction of the average field part. It's not going to be fun, to be honest. In particular, by the time you've built up your expression tree, that's not going to be statically typed in the same way as a normal query expression would be, due to the inability to express the anonymous type in a declaration.

If you're using .NET 4, dynamic typing may help you, although you'd pay the price of not having static typing any more, of course.

One option (horrible though it may be) would be try to use a sort of "template" of the anonymous type projection expression tree (e.g. always using a single property), and then build a copy of that expression tree, inserting the right expression instead. Again, it's not going to be fun.

Marc Gravell may be able to help more on this - it does sound like the kind of thing which should be possible, but I'm at a loss as to how to do it elegantly at the moment.

于 2011-03-22T17:40:37.357 回答
0

Eh? the parameter to Queryable.Average is not Func<T, U>. It's Expression<Func<T, U>>

The way to do this is:

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!");
} 

Followed by:

Expression<Func<TestGuy, int>> averageExpr = GetAverageExpr(someCondition);
var byCity = testGuys
  .GroupBy(c => c.City)
  .Select(g => new { City = g.Key, Avg = g.Average(averageExpr) });
于 2011-03-07T21:07:14.213 回答