使用LINQKit实现IQueryable扩展方法后子查询无法翻译如何解决
背景
我希望通过封装扩展方法优化代码,了解到LINQKit提供的Expandable功能后,实现了简单的扩展方法AtPointInTime,该方法接收DateTimeOffset类型入参,用于筛选实现了IBitemporal接口的双时态数据,但运行时一直报错,查阅官方文档也未找到解决方案,请问我哪里出错了?
实现代码
[Expandable(nameof(AtPointInTimeImpl))] public static IQueryable<TSource> AtPointInTime<TSource>(this IQueryable<TSource> query, DateTimeOffset pointInTime) where TSource : class, IBitemporal { return query.AsExpandable().Where(entity => AtPointInTimeFilter<TSource>().Invoke(entity, pointInTime)); } private static Expression<Func<IQueryable<TSource>, DateTimeOffset, IQueryable<TSource>>> AtPointInTimeImpl<TSource>() where TSource : class, IBitemporal { return (query, pointInTime) => query.Where(entity => AtPointInTimeFilter<TSource>().Expand().Invoke(entity, pointInTime)); } private static Expression<Func<TSource, DateTimeOffset, bool>> AtPointInTimeFilter<TSource>() where TSource : class, IBitemporal { return (entity, pointInTime) => entity.ValidTimeFrom <= pointInTime && (entity.ValidTimeTo > pointInTime || entity.ValidTimeTo == null); }
使用方式
我通过调用AtPointInTime获取指定时间点的所有公司数据(注:基于双时态存储方案实现)。
由于
AtPointInTime需要支持在子查询中调用,因此我给该方法添加了Expandable标记。
示例1
var companies = await _dbContext.Companies.AtPointInTime(DateTimeOffset.UtcNow).ToList()
示例2
var query = from alarm in _dbContext.Alarms.AtPointInTime(request.PointInTime) select new GetAlarmQueryResult { Alarm = alarm, Company = _dbContext.Companies.AtPointInTime(alarm.Created).SingleOrDefault(), };
报错信息
LINQ表达式无法翻译: 'DbSet<Company> .Where(c => (entity, pointInTime) => (DateTimeOffset)entity.ValidTimeFrom <= pointInTime && (Nullable<DateTimeOffset>)entity.ValidTimeTo > (Nullable<DateTimeOffset>)pointInTime || entity.ValidTimeTo == null .Invoke( expr: c, arg1: __pointInTime_0))' 请重写为可翻译的查询形式,或者通过插入AsEnumerable()、AsAsyncEnumerable()、ToList()、ToListAsync()等调用显式切换到客户端评估。
解决方案
错误原因
EF Core收到的表达式树中存在未被展开的Invoke调用,无法转换为SQL语句,说明LINQKit没有完成全量的表达式展开。你的实现存在三个问题:
- 在
AtPointInTime扩展方法内部调用了AsExpandable,该方法仅需在整个查询的最根层调用一次即可,内部多次调用会导致子查询场景下的表达式展开链路断裂。 - 每次调用都重新生成过滤表达式,不仅影响性能,也可能导致LINQKit的表达式匹配逻辑失效。
AtPointInTimeImpl中多余的Expand调用干扰了LINQKit的默认展开逻辑。
修正后代码
using System.Collections.Concurrent; using LinqKit; using Microsoft.EntityFrameworkCore; public static class BitemporalQueryExtensions { // 缓存不同实体类型的过滤表达式,避免重复生成 private static readonly ConcurrentDictionary<Type, object> _filterCache = new ConcurrentDictionary<Type, object>(); [Expandable(nameof(AtPointInTimeImpl))] public static IQueryable<TSource> AtPointInTime<TSource>(this IQueryable<TSource> query, DateTimeOffset pointInTime) where TSource : class, IBitemporal { var filter = GetFilter<TSource>(); return query.Where(entity => filter.Invoke(entity, pointInTime)); } private static Expression<Func<IQueryable<TSource>, DateTimeOffset, IQueryable<TSource>>> AtPointInTimeImpl<TSource>() where TSource : class, IBitemporal { var filter = GetFilter<TSource>(); return (query, pointInTime) => query.Where(entity => filter.Invoke(entity, pointInTime)); } private static Expression<Func<TSource, DateTimeOffset, bool>> GetFilter<TSource>() where TSource : class, IBitemporal { return (Expression<Func<TSource, DateTimeOffset, bool>>)_filterCache.GetOrAdd(typeof(TSource), _ => { return (entity, pointInTime) => entity.ValidTimeFrom <= pointInTime && (entity.ValidTimeTo > pointInTime || entity.ValidTimeTo == null); }); } }
调用方式调整
所有用到AtPointInTime的查询,仅需在最外层调用一次AsExpandable触发全局展开即可:
示例1修正
var companies = await _dbContext.Companies .AsExpandable() .AtPointInTime(DateTimeOffset.UtcNow) .ToListAsync();
示例2修正
var query = from alarm in _dbContext.Alarms.AsExpandable().AtPointInTime(request.PointInTime) select new GetAlarmQueryResult { Alarm = alarm, Company = _dbContext.Companies.AtPointInTime(alarm.Created).SingleOrDefault(), }; var result = await query.ToListAsync();
内容的提问来源于stack exchange,提问作者SOK
相关产品推荐
相关产品推荐

