自定义 LINQ GroupBy 帮助程序出现错误,并显示“此函数只能从 LINQ 调用到实体”
本文关键字:LINQ 函数 实体 调用 显示 帮助程序 GroupBy 错误 自定义 | 更新日期: 2023-09-27 18:20:57
我正在尝试使GroupBy
助手重载以简化常见的报告案例: 按日期(日、周、月、年等(分组
这是我的代码:
IEnumerable<TResult> GroupByDate<TSource, TResult>(IEnumerable<TSource> source,
DateTime startDate,
DateTime endDate,
ReportDateType dateType,
Func<TSource, System.DateTime?> keySelector,
Func<ReportDate, IEnumerable<TSource>, TResult> resultSelector) where TResult: ReportRow, new()
{
var results = from x in source
group x by
(dateType == ReportDateType.Day ? new ReportDate() { Year = keySelector(x).Value.Year, Month = keySelector(x).Value.Month, Week = SqlFunctions.DatePart("week", keySelector(x)) ?? 0, Day = keySelector(x).Value.Day } :
dateType == ReportDateType.Week ? new ReportDate() { Year = keySelector(x).Value.Year, Month = keySelector(x).Value.Month, Week = SqlFunctions.DatePart("week", keySelector(x)) ?? 0, Day = 0 } :
dateType == ReportDateType.Month ? new ReportDate() { Year = keySelector(x).Value.Year, Month = keySelector(x).Value.Month, Week = 0, Day = 0 } :
new ReportDate() { Year = keySelector(x).Value.Year, Month = 0, Week = 0, Day = 0 }) into g
orderby g.Key
select resultSelector(g.Key, g);
var resultsGap = new List<TResult>();
var currentDate = startDate;
while (currentDate <= endDate)
{
var reportRow = (TResult)Activator.CreateInstance(typeof(TResult));
if (dateType == ReportDateType.Day)
{
reportRow.Date = new ReportDate() { Year = currentDate.Year, Month = currentDate.Month, Week = currentDate.GetIso8601WeekOfYear(), Day = currentDate.Day };
currentDate = currentDate.AddDays(1);
}
else if (dateType == ReportDateType.Week)
{
reportRow.Date = new ReportDate() { Year = currentDate.Year, Month = currentDate.Month, Week = currentDate.GetIso8601WeekOfYear() };
currentDate = currentDate.AddDays(7);
}
else if (dateType == ReportDateType.Month)
{
reportRow.Date = new ReportDate() { Year = currentDate.Year, Month = currentDate.Month };
currentDate = currentDate.AddMonths(1);
}
else if (dateType == ReportDateType.Year)
{
reportRow.Date = new ReportDate() { Year = currentDate.Year };
currentDate = currentDate.AddYears(1);
}
resultsGap.Add(reportRow);
}
return results.ToList().Union(resultsGap, new ReportRowComparer<TResult>()).OrderBy(o => o.Date);
}
示例用例:
var results = GroupByDate(db.Orders, startDate.Value, endDate.Value, ReportDateType.Week, k => k.DateOrdered,
(k, g) => new RevenueReportRow() { Date = k, Revenue = g.Sum(i => i.Total), Cost = g.Sum(i => i.Cost), Profit = g.Sum(i => i.Total) - g.Sum(i => i.Cost) })
我遇到了异常:
System.NotSupportedException was unhandled by user code
HResult=-2146233067
Message=This function can only be invoked from LINQ to Entities.
Source=EntityFramework.SqlServer
StackTrace:
at System.Data.Entity.SqlServer.SqlFunctions.DatePart(String datePartArg, Nullable`1 date)
上面的代码在不是辅助方法时有效,当我用 Week = 0
替换Week = SqlFunctions.DatePart("week", keySelector(x)) ?? 0
时有效,但总数是错误的......
我发现了一个类似的问题,它解决了使用谓词参数创建帮助程序的问题。不幸的是,我无法弄清楚如何应用相同的方法来指定group by
值。
编辑
对于后代,我已经完成了帮助程序,它在这里可用:https://gist.github.com/albertbori/4944fd0e78534782cae2
我真的很接近
不,你离得很远。
上面的代码在不是辅助方法时有效
这是因为帮助程序方法代码不等同于原始代码。主要区别在于原始代码在IQueryable<Source>
上运行,而帮助程序方法代码 - 在IEnumerable<TSoure>
上运行。这是构建基块基元和执行行为的根本变化。前者在Expression<Func<..
上运行,后者在Func<..
上操作。前者构建并执行数据库查询,后者在内存中执行所有操作。有些函数仅适用于前者,有些函数仅适用于后者。简而言之,它们不能自由互换。
因此,为了使它们等效,您的帮助程序必须对 IQueryable<TSource>
进行操作,这反过来又需要参数Expression<...
类型。
现在,您应该问自己的第一件事是帮助程序方法是否足够值得。使用表达式并不像使用函数那么容易,它需要对System.Linq.Expressions
有很好的了解。此外,由于表达式必须由代码创建,因此不能再使用 LINQ 查询语法,因此您需要充分了解 Queryable
方法及其与 LINQ 语法构造的映射。
综上所述,并假设它是值得的,让我们看看我们如何解决具体问题。
(1( source
参数类型变为IQueryable<TSource>
(2( keySelector
参数类型变为Expression<Func<TSource, DateTime>>
。请注意,我不使用DateTime?
,您编写代码的方式暗示属性类型是DateTime
,并且您使其可为空,只能调用SqlFunctions.DatePart
。为了让生活更轻松,我们需要获得一个纯属性访问器,我们可以在需要时进行必要的转换。
(3( resultSelector
参数类型变为Expression<Func<IGrouping<ReportDate, TSource>, TResult>>
(4(现在是棘手的部分-group by
.我们需要一个表达式 将keySelector
转换为Expression<Func<TSource, ReportDate>>
.在这里,编码终于开始了:
static Expression<Func<TSource, ReportDate>> ToReportDate<TSource>(Expression<Func<TSource, DateTime>> keySelector, ReportDateType dateType)
{
var source = keySelector.Parameters[0];
var member = keySelector.Body;
var year = Expression.Property(member, "Year");
var month = dateType == ReportDateType.Day || dateType == ReportDateType.Week || dateType == ReportDateType.Month ? (Expression)
Expression.Property(member, "Month") :
Expression.Constant(0);
var week = dateType == ReportDateType.Day || dateType == ReportDateType.Week ? (Expression)
Expression.Convert(Expression.Call(typeof(SqlFunctions), "DatePart", null,
Expression.Constant("week"), Expression.Convert(member, typeof(DateTime?))
), typeof(int)) :
Expression.Constant(0);
var day = dateType == ReportDateType.Day ? (Expression)
Expression.Property(member, "Day") :
Expression.Constant(0);
var dateSelector = Expression.Lambda<Func<TSource, ReportDate>>(
Expression.MemberInit(
Expression.New(typeof(ReportDate)),
Expression.Bind(typeof(ReportDate).GetProperty("Year"), year),
Expression.Bind(typeof(ReportDate).GetProperty("Month"), month),
Expression.Bind(typeof(ReportDate).GetProperty("Week"), week),
Expression.Bind(typeof(ReportDate).GetProperty("Day"), day)
), source);
return dateSelector;
}
如果您不理解上面的代码,请再次阅读"值得"和"所需知识"部分。
最后,帮助程序方法变成这样
static IEnumerable<TResult> GroupByDate<TSource, TResult>
(
IQueryable<TSource> source,
DateTime startDate,
DateTime endDate,
ReportDateType dateType,
Expression<Func<TSource, DateTime>> keySelector,
Expression<Func<IGrouping<ReportDate, TSource>, TResult>> resultSelector
)
where TResult : ReportRow, new()
{
var results = source
.GroupBy(ToReportDate(keySelector, dateType))
.OrderBy(g => g.Key)
.Select(resultSelector);
// the rest of the code
}
和示例用法
var results = GroupByDate(db.Orders, startDate.Value, endDate.Value, ReportDateType.Week, k => k.DateOrdered,
g => new RevenueReportRow() { Date = g.Key, Revenue = g.Sum(i => i.Total), Cost = g.Sum(i => i.Cost), Profit = g.Sum(i => i.Total) - g.Sum(i => i.Cost) });
因为您将输入和输出声明为 IEnumerable<T>
所以调用该方法会使您脱离实体框架的上下文,进入 LINQ to Objects 世界,在该环境中,查询在内存中运行。在该内存中查询中,不能再使用 SqlFunctions
。
可以将输入和输出从 IEnumerable<T>
更改为IQueryable<T>
,从Func<T>
更改为Expresion<Func<T>>
,以使其成为实体框架查询的一部分,但方法使用的内容 EF 将无法转换为正确的 SQL 查询(Activator.CreateInstance
、while
循环等(,因此它也不起作用。你只会得到一个不同的错误。
您现在有两种方法可以解决问题:
- 停止使用
SqlFunctions
并使其成为正确的 LINQ to 对象内存中查询 - 将输入/输出参数更改为使用
IQueryable<T>
和Expression<>
而不是IEnumerable<T> and
Func<>',并使 EF 可以将逻辑转换为 SQL。
请注意,如果您选择第一个选项并执行
var results = GroupByDate(db.Orders, startDate.Value, endDate.Value, ReportDateType.Week, k => k.DateOrdered,
(k, g) => new RevenueReportRow() { Date = k, Revenue = g.Sum(i => i.Total), Cost = g.Sum(i => i.Cost), Profit = g.Sum(i => i.Total) - g.Sum(i => i.Cost) })
它会将整个Orders
表放入应用程序的内存中,并将分组作为 LINQ to Objects 查询运行。这几乎是100%的情况下,这不是你想要的。