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

使用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没有完成全量的表达式展开。你的实现存在三个问题:

  1. 在AtPointInTime扩展方法内部调用了AsExpandable,该方法仅需在整个查询的最根层调用一次即可,内部多次调用会导致子查询场景下的表达式展开链路断裂。
  2. 每次调用都重新生成过滤表达式,不仅影响性能,也可能导致LINQKit的表达式匹配逻辑失效。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.25 00:27:02