通过扩展方法过滤子查询

Posted

技术标签:

【中文标题】通过扩展方法过滤子查询【英文标题】:Subquery filtering through extension-method 【发布时间】:2021-12-31 23:08:18 【问题描述】:

背景

我正在尝试通过将一些代码提取到一些扩展方法中来清理我的代码,如下所示。我偶然发现了具有Expandable 功能的LINQKit。我创建了一个简单的扩展方法AtPointInTime,它以DateTimeOffset 作为输入(参见下面的代码),但我一直收到错误消息。我在做什么错(在文档中找不到解决方案)?

实施

[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 也可以在子查询中调用,因此我将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(),
            ;

错误

The LINQ expression '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))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

【问题讨论】:

我知道 LINQKit 是如何工作的,但我试图理解为什么如此复杂的解决方案。这里。展示您打算如何使用此扩展程序。 嗨@SvyatoslavDanyliv:我正确地误解了一些LinqKit 概念。我基本上只想要一个名为AtPointInTime 的扩展方法,它采用DateTimeOffset 并应用Where 子句。 AtPointInTime 扩展方法将用于子查询,因此我查看了Expandable。如何简化我的提案?感谢您抽出宝贵时间。 【参考方案1】:

尝试以下实现:

[Expandable(nameof(AtPointInTimeImpl))]
public static IQueryable<TSource> AtPointInTime<TSource>(this IQueryable<TSource> query, DateTimeOffset pointInTime) 
    where TSource : class, IBitemporal

    return query.AsExpandable().Where(entity => entity.AtPointInTime(pointInTime));


private static Expression<Func<IQueryable<TSource>, DateTimeOffset, IQueryable<TSource>>> AtPointInTimeImpl<TSource>() 
    where TSource : class, IBitemporal

    return (query, pointInTime) => query.Where(entity => entity.AtPointInTime(entity, pointInTime));


[Expandable(nameof(AtPointInTimeEntityImpl))]
public static bool AtPointInTime<TSource>(this TSource entity, DateTimeOffset pointInTime) 
    where TSource : class, IBitemporal

    throw new NotImplementedException();


private static Expression<Func<TSource, DateTimeOffset, bool>> AtPointInTimeEntityImpl<TSource>() 
    where TSource : class, IBitemporal

    return (entity, pointInTime) => entity.ValidTimeFrom <= pointInTime && (entity.ValidTimeTo > pointInTime || entity.ValidTimeTo == null);

如果配置 EF Core 选项,还可以消除 AsExpandable

builder
    .UseSqlServer(connectionString)
    .WithExpressionExpanding(); // enabling LINQKit extension

【讨论】:

【参考方案2】:

要消除的几件事:

这个表达式:

return (entity, pointInTime) => entity.ValidTimeFrom <= pointInTime && (entity.ValidTimeTo > pointInTime || entity.ValidTimeTo == null);

应该是:

return (entity, pointInTime) => entity.ValidTimeFrom <= pointInTime && (entity.ValidTimeTo == null || entity.ValidTimeTo > pointInTime);

反之,它会在空检查之前在比较中评估可空属性。 EF 可能能够生成,但可能不会。

我使用AsExpandable 的地方一直是DbSet,而不是IQueryable。尝试将 AsExpandable 移出扩展方法并调用:

var companies = await _dbContext.Companies
    .AsExpandable()
    .AtPointInTime(DateTimeOffset.UtcNow)
    .ToListAsync() 

如果可行,则可能需要更新扩展方法以针对DbSet&lt;TEntity&gt; 而不是IQueryable&lt;TEntity&gt;。我怀疑_dbContext.Companies.AsQueryable().AtPointInTime(...) 会起作用,尽管它可能值得一试? :)

其他看起来应该用于包装临时检查。如果我不得不冒险猜测扩展中的 AsExpandable() 调用而不是直接在 DbSet 上调用意味着 AsExpandable 没有在表达式构建器中“接受”,或者反转条件可能会按原样传递并且搞砸了一代人。

我假设普通的旧手动 linq 表达式可以正常工作?

var pointInTime = DateTimeOffset.UtcNow;

var companies = await _dbContext.Companies
    .Where(entity => entity.ValidTimeFrom <= pointInTime 
        && (entity.ValidTimeTo == null 
            || entity.ValidTimeTo > pointInTime)
    .ToListAsync();

【讨论】:

以上是关于通过扩展方法过滤子查询的主要内容,如果未能解决你的问题,请参考以下文章

如何简化和扩展 Firebase 数据库上的 BigQuery 子过滤 event_params

在过滤器数组上使用扩展运算符创建 Firestore 查询

你可以扩展 SQLAlchemy 查询类并在同一个会话中使用不同的类吗?

是否有比此查询更具可扩展性的子选择替代方案?

通过比较返回 lambda 表达式的扩展方法

通过思科模拟器CISCO PACKET TRACER学习网络11——扩展访问控制列表