NHibernate 3 LINQ - 如何为 Average() 创建有效参数

发布于 2024-10-20 20:41:52 字数 3027 浏览 2 评论 0 原文

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

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。

我的问题希望很简单 - 当 NHibernate 3.0 LINQ 支持(.Query() 方法)将其转换为 SQL 时,我的 GetAverageField 函数如何返回可用作 Average() 参数的东西?

欢迎任何建议,谢谢!

编辑

根据David B在回答中的评论,我仔细研究了这一点。我认为 Func 是正确的返回类型是基于我为 Average() 方法获得的智能感知。它似乎是基于 Enumerable 类型,而不是 Queryable 类型。这很奇怪……需要仔细观察一下。

GroupBy 方法具有以下返回签名:

IQueryable<IGrouping<string,TestGuy>>

这意味着它应该给我一个 IQueryable,好吧。但是,然后我继续进行下一行:

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

如果我检查 new { } 对象定义中 g 变量的智能感知,它实际上被列为 IGrouping 类型 - 而不是 IQueryable>。这就是为什么调用的 Average() 方法是 Enumerable 方法,以及它不接受 David B 建议的 Expression 参数的原因。

因此,不知何故,我的组值显然在某处失去了 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.

令我困惑的是,当我将 lambda 表达式传递给 Average() 方法时,这会起作用,但我可以'找到一种简单的方法来将相同的表达式表示为参数。我显然做错了什么,但看不到什么......!?

我已经束手无策了。 帮助我,乔恩·斯基特,你是我唯一的希望!;)

Say I have a very simple entity like this:

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;}
}

This contrived example object is mapped with NHibernate (using Fluent) and works fine.

Time to do some reporting. In this example, "testGuys" is an IQueryable with some criteria already applied.

var byCity = testGuys
    .GroupBy(c => c.City)
    .Select(g => new { City = g.Key, Avg = g.Average(tg => tg.InterestingValue) });

This works just fine. In NHibernate Profiler I can see the correct SQL being generated, and the results are as expected.

Inspired by my success, I want to make it more flexible. I want to make it configurable so that the user can get the average of OtherValue as well as InterestingValue. Shouldn't be too hard, the argument to Average() seems to be a Func (since the values are ints in this case). Easy peasy. Can't I just create a method that returns a Func based on some condition and use that as an argument?

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

And then, elsewhere, I could just do this:

var byCity = testGuys
    .GroupBy(c => c.City)
    .Select(g => new { City = g.Key, Avg = g.Average(fieldToAverageBy) });

Well, I thought I could do that. However, when I do enumerate this, NHibernate throws a fit:

Object of type 'System.Linq.Expressions.ConstantExpression' cannot be converted to type 'System.Linq.Expressions.LambdaExpression'.

So I am guessing that behind the scenes, some conversion or casting or some such thing is going on that in the first case accepts my lambda, but in the second case makes into something NHibernate can't convert to 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! ;)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

染柒℉ 2024-10-27 20:41:52

您将无法在 lambda 表达式中调用“本地”方法。如果这是一个简单的非嵌套子句,那么它会相对简单 - 您只需将 this: 更改

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

为 this:

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

,然后将调用结果传递到相关查询方法中,例如

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

,但是在这种情况下,您需要为 Select 子句构建整个表达式树 - 匿名类型创建表达式、Key 的提取以及平均字段部分的提取。老实说,这不会很有趣。特别是,当您构建表达式树时,由于无法在声明中表达匿名类型,因此它不会像普通查询表达式那样静态类型化。

如果您使用 .NET 4,动态类型可能可以帮助您,当然,您会付出不再使用静态类型的代价。

一种选择(尽管可能很糟糕)是尝试使用匿名类型投影表达式树的一种“模板”(例如始终使用单个属性),然后构建该表达式树的副本,插入正确的表达式反而。再说一次,这不会很有趣。

马克·格拉维尔(Marc Gravell)也许能够在这方面提供更多帮助 - 这听起来确实像是应该可能实现的事情,但我目前不知道如何优雅地做到这一点。

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.

一曲琵琶半遮面シ 2024-10-27 20:41:52

呃? Queryable.Average 的参数不是 Func。它是 Expression>

执行此操作的方法是:

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

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) });
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文