适用于 EF Core 时强制 SUM 方法为 return NULL
Force SUM method to return NULL when applicable in EF Core
我正在执行一个查询,其中我偶尔期望像这样的 NULL:
.Where(d => d.Id == varid && d.Date >= vardate1 && d.Date <= vardate2)
.Sum(d => (decimal?)d.Delta);
Delta 是不可为 null 的小数,智能感知显示 Sum 的结果将是小数?因为我介绍了演员。生成的 SQL 是预期的,当手动 运行 时它正确地 returns NULL 当没有匹配的记录时。但是,具体化查询的结果始终为 0。此行为不同于非核心 EF,后者 returned null。这真的是新的预期行为吗?如果是这样,我如何在需要时将其强制为 return null? Null 和 0 在此上下文中具有不同的含义。
我可以先引入记录,然后在服务器上求和,但如果 EF core 能自己完成我期望的事情就好了。
很可能是一个错误,但了解 EF Core 设计者对不可空 Max
/ Min
/ Average
以及 First
/ Single
翻译的愿景,如果他们故意这样做是为了模拟(奇怪的)LINQ to Objects nullable Sum
行为 returns 0
事件,尽管方法的结果类型是 nullable .
可以通过下面的代码片段看出
decimal? result = Enumerable.Empty<decimal?>().Sum(); // result is 0
偶数 documented (!?):
Remarks
This method returns zero if source
contains no elements.
"funny" 的事情是,这仅用于根查询 Sum
执行 - 在投影内部它具有您正在寻找的 SQL 行为。
这导致我们通过利用 group by constant 技巧结合投影来解决问题。为了不在你需要的地方重复它,并且如果它在以后的 EF Core 版本中得到修复也可以轻松删除它,你可以将它封装在自定义扩展方法中,如下所示:
public static partial class EfCoreExtensions
{
public static decimal? SumOrDefault<T>(this IQueryable<T> source, Expression<Func<T, decimal?>> selector)
=> source.GroupBy(e => 0, selector).Select(g => g.Sum()).AsEnumerable().FirstOrDefault();
}
并替换
.Sum(d => (decimal?)d.Delta);
和
.SumOrDefault(d => d.Delta);
只需确保仅将它用于最终调用,因为如果在查询表达式树中使用它,作为任何自定义方法,它都不会被识别并会导致客户端评估或运行时异常。
上面的“group by constant trick”在 EF Core 5.0 中不起作用。
使用聚合函数的扩展方法的变体可以达到预期的结果。
所以要让所有 NULL return 为空,否则 return 非空值的总和:
public static decimal? SumOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal?> selector)
=> (from s in source select selector(s))
.Aggregate((decimal?)null, (acc, item) => acc.HasValue ? acc + item.GetValueOrDefault() : item);
或者如果您希望任何 NULL 值都生成 NULL returned
public static decimal? SumAllOrNull<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal?> selector)
=> (from s in source select selector(s))
.Aggregate((decimal?)null, (acc, item) => acc.HasValue ? acc + item : item.HasValue ? item : null);
但请注意,如前所述,这仅适用于 Linq-to-Objects,不适用于 Linq-to-Sql,因此您需要事先使用 ToList() 或 AsEnumerable(),因此它带来了更多从数据库返回您可能想要或需要的数据。
.Where(d => d.Id == varid && d.Date >= vardate1 && d.Date <= vardate2)
.AsEnumerable()
.SumOrDefault(d => d.Delta);
我正在执行一个查询,其中我偶尔期望像这样的 NULL:
.Where(d => d.Id == varid && d.Date >= vardate1 && d.Date <= vardate2)
.Sum(d => (decimal?)d.Delta);
Delta 是不可为 null 的小数,智能感知显示 Sum 的结果将是小数?因为我介绍了演员。生成的 SQL 是预期的,当手动 运行 时它正确地 returns NULL 当没有匹配的记录时。但是,具体化查询的结果始终为 0。此行为不同于非核心 EF,后者 returned null。这真的是新的预期行为吗?如果是这样,我如何在需要时将其强制为 return null? Null 和 0 在此上下文中具有不同的含义。
我可以先引入记录,然后在服务器上求和,但如果 EF core 能自己完成我期望的事情就好了。
很可能是一个错误,但了解 EF Core 设计者对不可空 Max
/ Min
/ Average
以及 First
/ Single
翻译的愿景,如果他们故意这样做是为了模拟(奇怪的)LINQ to Objects nullable Sum
行为 returns 0
事件,尽管方法的结果类型是 nullable .
可以通过下面的代码片段看出
decimal? result = Enumerable.Empty<decimal?>().Sum(); // result is 0
偶数 documented (!?):
Remarks
This method returns zero if
source
contains no elements.
"funny" 的事情是,这仅用于根查询 Sum
执行 - 在投影内部它具有您正在寻找的 SQL 行为。
这导致我们通过利用 group by constant 技巧结合投影来解决问题。为了不在你需要的地方重复它,并且如果它在以后的 EF Core 版本中得到修复也可以轻松删除它,你可以将它封装在自定义扩展方法中,如下所示:
public static partial class EfCoreExtensions
{
public static decimal? SumOrDefault<T>(this IQueryable<T> source, Expression<Func<T, decimal?>> selector)
=> source.GroupBy(e => 0, selector).Select(g => g.Sum()).AsEnumerable().FirstOrDefault();
}
并替换
.Sum(d => (decimal?)d.Delta);
和
.SumOrDefault(d => d.Delta);
只需确保仅将它用于最终调用,因为如果在查询表达式树中使用它,作为任何自定义方法,它都不会被识别并会导致客户端评估或运行时异常。
上面的“group by constant trick”在 EF Core 5.0 中不起作用。
使用聚合函数的扩展方法的变体可以达到预期的结果。 所以要让所有 NULL return 为空,否则 return 非空值的总和:
public static decimal? SumOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal?> selector)
=> (from s in source select selector(s))
.Aggregate((decimal?)null, (acc, item) => acc.HasValue ? acc + item.GetValueOrDefault() : item);
或者如果您希望任何 NULL 值都生成 NULL returned
public static decimal? SumAllOrNull<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal?> selector)
=> (from s in source select selector(s))
.Aggregate((decimal?)null, (acc, item) => acc.HasValue ? acc + item : item.HasValue ? item : null);
但请注意,如前所述,这仅适用于 Linq-to-Objects,不适用于 Linq-to-Sql,因此您需要事先使用 ToList() 或 AsEnumerable(),因此它带来了更多从数据库返回您可能想要或需要的数据。
.Where(d => d.Id == varid && d.Date >= vardate1 && d.Date <= vardate2)
.AsEnumerable()
.SumOrDefault(d => d.Delta);