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
相关产品推荐
相关产品推荐

