【问题标题】:Build EF core Where() predicate at runtime在运行时构建 EF 核心 Where() 谓词
【发布时间】:2023-01-18 18:06:55
【问题描述】:

我写了一段代码,它在运行时构建了一个 EF Core Where() 谓词。我以前从未这样做过,但出于要求我不得不这样做。

要求只是在 CreatedOnUpdatedOn 的 7 天内从数据库中获取数据。

重构前的版本是循环调用数据库7次,存在性能问题。

当前的实现按预期工作并返回预期结果。

这是当前代码:

private async Task SevenDaysCashOutFloor(DateTimeOffset today, IQueryable<BillPaymentVoucher> pastBillPayments, IQueryable<JournalVoucherPaymentVoucher> pastJournalVoucherPayments, CancellationToken token)
{
        Expression<Func<BillPaymentVoucher, bool>> predicate = null!;
        Expression<Func<BillPaymentVoucher, bool>> aggregatedPredicate = null!;
        BinaryExpression binaryExpression = null!;

        var param = Expression.Parameter(typeof(BillPaymentVoucher));

        today = DateTimeOffset.UtcNow;

        for (int days = 0; days < 7; days++)
        {
            var date = today.AddDays(-days);

            predicate = (entity) => (
                                        (entity.UpdatedOn.HasValue && entity.UpdatedOn.Value.Date == date.Date) ||
                                        (entity.UpdatedOn.HasValue == false && entity.CreatedOn.Date == date.Date)
                                    );

            binaryExpression = Expression.OrElse(ExpressionReplacer.GetBody(aggregatedPredicate ?? predicate, param),
                                                 ExpressionReplacer.GetBody(predicate, param));

            aggregatedPredicate = Expression.Lambda<Func<BillPaymentVoucher, bool>>(binaryExpression, param);
        }

        var finalPredicate = Expression.Lambda<Func<BillPaymentVoucher, bool>>(binaryExpression, param);

        pastBillPayments = pastBillPayments.Where(finalPredicate);          
}

我正在运行时构建谓词,Where()for 循环中需要它。

一切都按预期工作,但我想知道:

  1. 这是否是正确的写法?

  2. 我应该使用复杂的Expression Tree吗?

  3. 有没有一种简单的方法可以实现这一目标?

  4. 这段代码可以重构吗?

    我以前从未使用过表达式树。

【问题讨论】:

  • 如果您的代码有效,它可能不属于 SO。您应该尝试将其发布到Code Review
  • 您知道您在谓词中不仅限于 ==!=,不是吗?您不需要进行 7 次单独比较来确定过去 7 天内是否发生了什么,只需使用 &gt;=
  • @Damien_The_Unbeliever,随着时间的推移,7 可能会发生变化,它已经发生了几次变化
  • 但是,整个事情仍然应该像var notBefore = DateTimeOffset.UtcNow.AddDays(-7).Date; pastBillPayments = pastBillPayments.Where(e=&gt;e.UpdateOn.HasValue &amp;&amp; e.UpdateOn.Value &gt;= notBefore || e.CreatedOn &gt;= notBefore);,除非你的数据包括未来的日期,在这种情况下它会稍微复杂一些,但不是你正在构建的。
  • Where 总是在运行时构造。没理由用entity.UpdatedOn.HasValue &amp;&amp; entity.UpdatedOn.Value.Date,用entity.UpdatedOn?.Date == date.Date就够了。这将被转换为 SQL,例如 cast(UpdatedOn as date) ==@d1。实际情况应该是entity.UpdatedOn?.Date == date.Date || entity.CreatedOn?.Date == date.Date。整个表达式循环可以替换为与多个值进行比较的单个表达式。甚至listOfDates.Contains(entity.UpdatedOn) || listOfDates.Contains(entity.CreatedOn)

标签: c# linq lambda entity-framework-core expression-trees


【解决方案1】:

Where 总是在运行时构造。也不需要显式检查空值。这些表达式将被转换为对 NULL 值有明确支持的 SQL。

此方法搜索其UpdatedOnCreatedOn 落在天数范围内的行。您可以通过创建那些日子的列表并使用 Contains 来替换它:

var daysInWeek=Enumerable.Range(0,7)
                         .Select(i=>DateTime.Today.AddDays(-i))
                         .ToList();

pastBillPayments = pastBillPayments.Where(p=>daysInWeek.Contains(p.UpdatedOn.Date) 
                          || daysInWeek.Contains(p.CreatedOn.Date) );

这将产生:

WHERE ... cast(UpdatedOn as date) IN (@d1,@d2,...,@d7) OR 
          cast(CreatedOn as date) IN (@d1,@d2,...,@d7)

但这不是最优的。虽然某些数据库(例如SQL Server)可以将cast(UpdatedOn as date) = ... 转换为可以使用索引的范围搜索,但它们不能使用索引统计信息。

更好的查询是在日期范围内明确搜索:

var dateTo=DateTime.Today.AddDays(1);
var dateFrom=DateTime.Today.AddDays(-6);

pastBillPayments = pastBillPayments
    .Where(p=> 
        (p.UpdatedOn >= dateFrom && p.UpdateOn  < dateTo) ||
        (p.CreatedOn >= dateFrom && p.CreatedOn < dateTo));

这消除了任何转换,允许服务器同时使用索引和统计信息

【讨论】:

  • 我认为它必须是 daysInWeek 中的 daysInWeek .Contains() 而不是 listOfDates.Contains 是不是?
猜你喜欢
  • 1970-01-01
  • 2011-02-07
  • 1970-01-01
  • 2019-01-14
  • 2022-11-23
  • 2014-04-01
  • 2013-08-05
  • 2015-12-24
  • 1970-01-01
相关资源
最近更新 更多