为啥我不能像使用列表一样过滤 IQueryable?

Posted

技术标签:

【中文标题】为啥我不能像使用列表一样过滤 IQueryable?【英文标题】:Why can't I filter an IQueryable like I can with a List?为什么我不能像使用列表一样过滤 IQueryable? 【发布时间】:2019-09-01 07:21:26 【问题描述】:

我正在稍微重构一个项目,然后又回到了一个我过去从未解决过的问题。我正在尝试对 EF Core db 的查询执行多个过滤器。

过去我曾尝试设置一系列 Where 语句来检查过滤器语句是否为空或通过匹配过滤器。

这在查询中的某处返回了 nullReferenceException。我通过在没有过滤器的情况下运行我的查询然后将过滤器应用到我的列表来解决了这个问题。

我回来创建了一个 WhereIf 扩展,希望它可以解决我的问题,同时让代码更简洁一些,但同样的问题出现了。

我目前尝试在查询上运行四个过滤器,并且它通过了初始过滤器,但如果选择了其他三个过滤器中的任何一个,则查询会出现 nullReferenceException。

如果我从一般查询和第一个过滤器中获取列表,然后将过滤器应用到我的列表,这同样有效。

这是我想做的:

IQueryable<Film> films = _context.Films
    .Include(f => f.Media)
    .Include(f=> f.Audio)
    .Include(f => f.FilmGenres)
        .ThenInclude(fg => fg.Genre)
    .WhereIf(!string.IsNullOrEmpty(vm.SearchValue), f => f.Name.ToLower().Contains(vm.SearchValue.ToLower()))
    .WhereIf(!string.IsNullOrEmpty(vm.MediaFilter), f => f.Media.Name == vm.MediaFilter)
    .WhereIf(!string.IsNullOrEmpty(vm.AudioFilter), f => f.Audio.Name == vm.AudioFilter)
    .WhereIf(!string.IsNullOrEmpty(vm.GenreFilter), f => f.FilmGenres.Any(fg => fg.Genre != null && fg.Genre.Name == vm.GenreFilter));

这里是 WhereIf 方法:

public static IQueryable<TSource> WhereIf<TSource>(this IQueryable<TSource> source, bool condition, Expression<Func<TSource, bool>> predicate)
        
            // Performs a Where only when the condition is met

            if (condition)
            
                source = source.Where(predicate);
                return source;
            

            return source;
        

vm.SearchValue 上的过滤器通过正常,当我逐步通过它时,该值是预期的 IQueryable。一旦它命中任何其他过滤器,它就会返回 nullReferenceException(稍后它最终到达 ToList() 时)。如果我在返回之前查看源的值,它会在结果视图中显示它具有空异常。

我已经尝试过每一行(使用电影 = film.Where(...))。我尝试跳过 WhereIf 并只执行 if 语句和标准 Where,所有这些都具有相同的结果。

只有当我创建一个 List 对象,由数据的一般查询填充,然后过滤该 List 对象时,我才能让它工作。

那么,在 EF Core 中过滤 IQueryable 有什么问题?这是不允许的,还是我做错了什么?

更新:所有 Film 对象都具有 Media/Audio/FilmGenre 对象,并且所有内容都已包含在内。并且我已经验证 IQueryable 源中的项目在 WhereIf 方法中的 Where 语句之前具有所有这些项目。

我尝试将每个过滤器语句分别分开,其中包括跳过 WhereIf 方法并同时使用 if 语句。

此外,一次只能选择一个过滤器(目前)。那些没有被选中的结果是条件为假,没有问题。只有在使用有源滤波器时才会打嗝。例如,我将进行仅检查 vm.SearchValue 的初始搜索。这会给我一个电影列表以及过滤和排序的选项。然后,当我选择按音频或媒体等进行过滤时,我遇到了问题。

这是堆栈跟踪:

   at lambda_method(Closure , InternalEntityEntry )
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.SimpleNonNullableDependentKeyValueFactory`1.TryCreateFromCurrentValues(InternalEntityEntry entry, TKey& key)
   at Microsoft.EntityFrameworkCore.Query.Internal.WeakReferenceIdentityMap`1.CreateIncludeKeyComparer(INavigation navigation, InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryBuffer.IncludeCore(Object entity, INavigation navigation)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryBuffer.Include(QueryContext queryContext, Object entity, IReadOnlyList`1 navigationPath, IReadOnlyList`1 relatedEntitiesLoaders, Int32 currentNavigationIndex, Boolean queryStateManager)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryBuffer.Include(QueryContext queryContext, Object entity, IReadOnlyList`1 navigationPath, IReadOnlyList`1 relatedEntitiesLoaders, Boolean queryStateManager)
   at Microsoft.EntityFrameworkCore.Query.Internal.GroupJoinInclude.GroupJoinIncludeContext.Include(Object entity)
   at Microsoft.EntityFrameworkCore.Query.Internal.GroupJoinInclude.GroupJoinIncludeContext.Include(Object entity)
   at Microsoft.EntityFrameworkCore.Query.Internal.GroupJoinInclude.GroupJoinIncludeContext.Include(Object entity)
   at Microsoft.EntityFrameworkCore.Query.Internal.GroupJoinInclude.GroupJoinIncludeContext.Include(Object entity)
   at Microsoft.EntityFrameworkCore.Query.QueryMethodProvider.<_GroupJoin>d__26`4.MoveNext()
   at System.Linq.Enumerable.<SelectManyIterator>d__165`3.MoveNext()
   at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
   at Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider.<_TrackEntities>d__15`2.MoveNext()
   at Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider.ExceptionInterceptor`1.EnumeratorExceptionInterceptor.MoveNext()
   at System.Collections.Generic.EnumerableHelpers.ToArray[T](IEnumerable`1 source, Int32& length)
   at System.Collections.Generic.EnumerableHelpers.ToArray[T](IEnumerable`1 source)
   at System.Linq.SystemCore_EnumerableDebugView`1.get_Items()

以下图片:

    这是在 WhereIf 中的 Where 语句之前通过 SearchValue 过滤器时源的结果视图 在此之后是 Where 语句 这里是通过 AudioFilter - 显示的谓词。 这是执行 AudioFilter 时 Where 语句之前的源 - 与 SearchValue 过滤之后相同 最后,在执行音频过滤时的位置之后

更新:已解决。还有另一项检查涉及导致客户端评估的应用程序用户,该检查已被移动,现在查询按预期工作。

【问题讨论】:

所有电影都肯定有媒体、音频和电影流派关联的实体吗?你包括他们吗?我猜你需要检查例如f =&gt; (f.Media != null) &amp;&amp; (f.Media.Name == vm.MediaFilter)。但我猜这里的 NPE 意味着这并没有被翻译成 SQL 以在数据库上执行,就像你可能希望的那样。 每次添加一个WhereIf。您正在做的事情是导致 EF Core 拆分查询并在 LINQ to Objects 中运行其中的一部分(它在转换为 SQL 方面不如 EF 6),这是您的核心 (npi) 问题。 它的显示方式应该可以工作。一定有什么东西引起了客户的评价。如果您配置 [可选行为:为客户端评估抛出异常](docs.microsoft.com/en-us/ef/core/querying/…) 会发生什么? @Rup 是的,所有这些项目都包括在内(并在调试期间验证它们是否存在)。 @IvanStoev 我想我现在明白了。这可以追溯到你提到的关于客户评估的内容。我忘了在这里添加另一个根据应用​​程序的用户执行的位置。一旦我把它拿出来放在别处,它就起作用了。我仍然不确定所有事情的时间以及为什么基于 SearchValue 的第一个过滤器也没有引起问题,但这似乎是罪魁祸首。这是我没有在这里展示那一点的坏处。感谢您的帮助。 【参考方案1】:

我总是只使用简单的 OR 运算符而不是 WhereIf

 IQueryable<Film> films = _context.Films
    .Include(x => x.Media)
    .Include(x => x.Audio)
    .Include(x => x.FilmGenres)
    .ThenInclude(g => g.Genre)
                .Where(f => string.IsNullOrEmpty(vm.SearchValue) || f.Name.ToLower().Contains(vm.SearchValue.ToLower()))
                .Where(f => string.IsNullOrEmpty(vm.MediaFilter) || f.Media.Name == vm.MediaFilter)
                .Where(f => string.IsNullOrEmpty(vm.AudioFilter) || f.Audio.Name == vm.AudioFilter)
                .Where(f => string.IsNullOrEmpty(vm.GenreFilter) || (f.FilmGenres.Any(fg => fg.Genre != null && fg.Genre.Name == vm.GenreFilter)));

【讨论】:

这是我之前(去年)尝试过的方法,但也遇到了同样的问题,采用了同样的修复方法……先获取列表,然后进行过滤。 FilmGenres、Media 和 Audio 是否可能为空?我知道如果使用.net core,默认情况下不启用延迟加载,所以你需要调用include。 不,他们有价值观。我可以尝试缩小我的 searchValue,使其对象数量非常少,并手动验证每个对象。 我刚刚验证了在应用 AudioFilter 之前存在的对象确实填充了所有必要的值。 啊,好吧...不确定发生了什么然后获得空引用。【参考方案2】:

这个答案是即兴的,是我的一些猜测,所以如果没有帮助,我深表歉意。

无论如何,有几件事对我来说很突出。

首先,您的 WhereIf() 函数 - 它并没有完全实现 Where() 的功能。 Where() 获取一个源并返回第二个源,其中记录集被筛选掉。值得注意的是,它根本不会更改原始数据源。好吧,您的 WhereIf() 正在尝试这样做 - 它正在更改传递给函数的“源”变量。我做了一些谷歌搜索,IQueryable doesn't 看起来是不可变的,这意味着它可以在不创建新类实例的情况下进行更改,所以我不确定这行代码没有搞砸为它的建筑打下基础:

source = source.Where(predicate);

...它会解释你得到的结果。第一个条件为真的 'WhereIf' 有效,但下一个无效 - 因为第一个与它正在处理的基础对象搞乱了。至少,您应该将其更改为“return source.Where(predicate)”,只是为了代码清晰(因为您现有的代码看起来像是 试图 更改它。)

其次,您是否尝试过拆分声明?我的意思是,像这样:

var results = SomeLinq.SomeStatement(a => something(a))
        .Where(b => b == something)
        .Where(c => c == something)

...与以下内容相同:

var mainQueryable = SomeLinq.SomeStatement(a => something(a));
var filtered = mainQueryable.Where(b => b == something);
var results = filtered.Where(c => c == something);

这反过来又可以让您为 LINQ 简化图片:

IQueryable<Film> films = _context.Films
    .Include(f => f.Media)
    .Include(f=> f.Audio)
    .Include(f => f.FilmGenres)
    .ThenInclude(fg => fg.Genre);
if (!string.IsNullOrEmpty(vm.SearchValue)) films = films.Where(f => f.Equals(vm.SearchValue, StringComparison.OrdinalIgnoreCase);
if (!string.IsNullOrEmpty(vm.MediaFilter)) films = films.Where(f => f.Media.Name == vm.MediaFilter);
// etc...

...所以最后的 LINQ 语句没有多余的 WHERE 子句,这些子句实际上不会过滤任何内容。

无论如何,希望这些能有所帮助。

【讨论】:

我实际上已经尝试了所有这些(除了为每次分离使用不同的变量)。 WhereIf 最初确实返回了 source.Where(predicate),但我添加了另一行进行调试,以便我可以看到返回的内容。还需要注意的是,在我使用 WhereIf 之前,使用 .Where(f => string.IsNullOrEmpty(vm.MediaFilter) || f.Media.Name == vm.MediaFilter) 具有相同的结果。 实际上,认为分配给source 是一个问题表明对C# 的工作原理缺乏了解。 source 是一个对象引用,重新分配它以指向不同的对象对原始对象没有任何作用。请参阅Where 的源代码,了解(功能上)相同的内容。

以上是关于为啥我不能像使用列表一样过滤 IQueryable?的主要内容,如果未能解决你的问题,请参考以下文章

在 Haskell 中,为啥没有 TypeClass 用于可以像列表一样的东西?

为啥 python 不能矢量化 map() 或列表推导

获取和过滤 iqueryable 对象内的多个类

为啥我不能像传递其他变量一样将函数从 Express.js 传递给 EJS?

使用 Distinct() 过滤 Linq 中的重复记录

为啥我不能像行一样简单地获得一维数组?