适用于 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);