You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

EF Core运行时构建Where谓词:写法正确性与优化咨询

关于EF Core运行时构建Where谓词的疑问与优化建议

背景说明

业务需求为从数据库获取CreatedOn或UpdatedOn在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);          
}

疑问解答与优化建议

1. 这种写法是否规范?

不算规范。存在几个明显问题:

  • 用null!强制初始化变量,存在潜在空引用风险,且降低代码可读性
  • 传入的today参数被直接覆盖为DateTimeOffset.UtcNow,属于无效参数,是代码异味
  • 循环中重复构建Lambda表达式再替换参数,逻辑冗余,若ExpressionReplacer实现有瑕疵,易引发参数不匹配问题

2. 是否必须使用复杂的Expression Tree?

完全没必要。EF Core支持直接编写LINQ表达式,手动构建表达式树仅适用于动态不确定条件数量的场景,而本次需求是固定的7天时间范围,属于静态条件范畴。

3. 有无更简单的实现方式?

有两种更高效简洁的方式:

  • 方式一:时间范围查询(推荐)
    直接计算7天前后的日期范围,用范围判断替代7个OR条件,EF Core可直接翻译成高效的SQL范围查询:

    var utcNow = DateTimeOffset.UtcNow;
    var sevenDaysAgo = utcNow.AddDays(-7).Date;
    var todayDate = utcNow.Date;
    
    pastBillPayments = pastBillPayments.Where(entity => 
        (entity.UpdatedOn.HasValue && entity.UpdatedOn.Value.Date >= sevenDaysAgo && entity.UpdatedOn.Value.Date <= todayDate)
        || 
        (!entity.UpdatedOn.HasValue && entity.CreatedOn.Date >= sevenDaysAgo && entity.CreatedOn.Date <= todayDate)
    );
    
  • 方式二:用PredicateBuilder简化拼接(非必要场景)
    若因特殊需求必须按天拼接条件,可使用LinqKit的PredicateBuilder简化表达式拼接,无需手动处理参数替换:

    var predicate = PredicateBuilder.New<BillPaymentVoucher>();
    var utcNow = DateTimeOffset.UtcNow;
    for (int days = 0; days < 7; days++)
    {
        var date = utcNow.AddDays(-days).Date;
        predicate = predicate.Or(entity => 
            (entity.UpdatedOn.HasValue && entity.UpdatedOn.Value.Date == date)
            || 
            (!entity.UpdatedOn.HasValue && entity.CreatedOn.Date == date)
        );
    }
    pastBillPayments = pastBillPayments.AsExpandable().Where(predicate);
    

    注:此方式性能不如范围查询,仅作备选。

4. 这段代码能否重构优化?

可以,核心优化方向包括移除冗余逻辑、规避空引用风险、提升可读性:

  • 重构后的范围查询版本(最优):

    private async Task SevenDaysCashOutFloor(IQueryable<BillPaymentVoucher> pastBillPayments, IQueryable<JournalVoucherPaymentVoucher> pastJournalVoucherPayments, CancellationToken token)
    {
        var utcNow = DateTimeOffset.UtcNow;
        var sevenDaysAgo = utcNow.AddDays(-7).Date;
        var todayDate = utcNow.Date;
    
        pastBillPayments = pastBillPayments.Where(entity => 
            (entity.UpdatedOn.HasValue && entity.UpdatedOn.Value.Date >= sevenDaysAgo && entity.UpdatedOn.Value.Date <= todayDate)
            || 
            (!entity.UpdatedOn.HasValue && entity.CreatedOn.Date >= sevenDaysAgo && entity.CreatedOn.Date <= todayDate)
        );
    
        // 补充原方法缺失的后续逻辑,如返回处理后的IQueryable
    }
    
  • 若坚持使用表达式树拼接(不推荐),重构后版本:

    private async Task SevenDaysCashOutFloor(IQueryable<BillPaymentVoucher> pastBillPayments, IQueryable<JournalVoucherPaymentVoucher> pastJournalVoucherPayments, CancellationToken token)
    {
        var utcNow = DateTimeOffset.UtcNow;
        var param = Expression.Parameter(typeof(BillPaymentVoucher));
        Expression<Func<BillPaymentVoucher, bool>> aggregatedPredicate = null;
    
        for (int days = 0; days < 7; days++)
        {
            var date = utcNow.AddDays(-days).Date;
            // 直接构建表达式体,避免Lambda参数替换
            var updatedOnProp = Expression.Property(param, nameof(BillPaymentVoucher.UpdatedOn));
            var updatedHasValue = Expression.Property(updatedOnProp, nameof(Nullable<DateTimeOffset>.HasValue));
            var updatedValueDate = Expression.Property(Expression.Property(updatedOnProp, nameof(Nullable<DateTimeOffset>.Value)), nameof(DateTimeOffset.Date));
            var dateConstant = Expression.Constant(date);
            var updatedCondition = Expression.AndAlso(updatedHasValue, Expression.Equal(updatedValueDate, dateConstant));
    
            var createdOnDate = Expression.Property(param, nameof(BillPaymentVoucher.CreatedOn), nameof(DateTimeOffset.Date));
            var noUpdatedCondition = Expression.AndAlso(Expression.Not(updatedHasValue), Expression.Equal(createdOnDate, dateConstant));
    
            var dailyPredicate = Expression.OrElse(updatedCondition, noUpdatedCondition);
    
            aggregatedPredicate = aggregatedPredicate == null 
                ? Expression.Lambda<Func<BillPaymentVoucher, bool>>(dailyPredicate, param)
                : Expression.Lambda<Func<BillPaymentVoucher, bool>>(Expression.OrElse(aggregatedPredicate.Body, dailyPredicate), param);
        }
    
        if (aggregatedPredicate != null)
        {
            pastBillPayments = pastBillPayments.Where(aggregatedPredicate);
        }
    }
    

    该版本移除了ExpressionReplacer依赖,直接构建表达式体,逻辑更清晰,规避了空引用风险。


内容的提问来源于stack exchange,提问作者thanzeel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 12:15:26