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

自定义IQueryable Contains方法失效:无法转换为SQL的问题排查

问题描述

尝试为IQueryable实现过滤功能,检查对象列表是否包含指定元素,相关代码如下:

var ratings = repository.GetAll<Rating>()
              .AsNoTracking()
              .Select(e => new RatingGridDto()
              {
                  Id = e.Id,
                  Sites = e.Locations.Select(x => x.Name).ToList(),
                  PurchaseOrderNumbers= e.PurchaseOrderNumbers//this is IEnumerable<string>
              });

数据库中的OrderNumber格式为:"[12345]","[5432]","[18859]"。

尝试多种Linq表达式及自定义Expression实现均报错无法转换为SQL,测试代码如下:

ratings = ratings.Where(e => e.PurchaseOrderNumbers.AsQueryable().Any(pon => pon.Contains(purchaseOrderFilter)));

ratings = ratings.Where(e => e.PurchaseOrderNumbers.Where(pon => pon.Contains(purchaseOrderFilter)).Any());
ratings = ratings.Where(e => e.PurchaseOrderNumbers.Contains("222"));
ratings = ratings.Where(e => e.PurchaseOrderNumbers.Any(pon => pon == "234234"));

ratings = ratings.Where(e => e.Sites.Any(s => s.Contains("a"))); // WORKS
ratings = ratings.Where(e => e.PurchaseOrderNumbers.Any(pon => pon.Contains("a"))); // FAILS

自定义方法代码:

public static IQueryable<T> OwnWhereIsContainedBy<T, K>(IQueryable<T> source, K elementToContain, string propertyName)
{
    if (string.IsNullOrEmpty(propertyName))
    {
        throw new ArgumentException("Property name cannot be null or empty.", nameof(propertyName));
    }

    ParameterExpression parameter = Expression.Parameter(typeof(T));
    MemberExpression property = Expression.Property(parameter, propertyName);
    ConstantExpression element = Expression.Constant(elementToContain);

    MethodInfo containsMethod = typeof(Enumerable).GetMethods()
        .Where(m => m.Name == "Contains" && m.GetParameters().Length == 2)
        .Single()
        .MakeGenericMethod(typeof(K));

    MethodCallExpression containsCall = Expression.Call(containsMethod, property, element);

    Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(containsCall, parameter);

    return source.Where(lambda);
}

相同逻辑的Sites过滤可正常工作,但PurchaseOrderNumbers的过滤全部失败,请问问题出在哪里?该如何解决?

问题原因及解决方案

核心原因

Sites和PurchaseOrderNumbers的底层映射逻辑存在本质差异:

  • Sites是通过e.Locations.Select(x => x.Name).ToList()生成的,对应数据库中关联表的查询,EF Core可以将其过滤逻辑转换为SQL中的关联查询+WHERE条件;
  • PurchaseOrderNumbers直接映射Rating实体的IEnumerable<string>属性,这通常对应数据库中的JSON类型字段或非标准集合存储字段,EF Core无法直接将针对内存IEnumerable的Any/Contains操作转换为SQL——数据库层面无法识别内存集合的操作逻辑。

解决方案

方案1:提前在数据库层面过滤(推荐)

不要先投影到RatingGridDto,先针对Rating实体完成过滤,再进行DTO投影:

var ratings = repository.GetAll<Rating>()
              .AsNoTracking()
              // 先针对实体属性过滤,EF Core可直接转换为SQL
              .Where(r => r.PurchaseOrderNumbers.Any(pon => pon.Contains(purchaseOrderFilter)))
              .Select(e => new RatingGridDto()
              {
                  Id = e.Id,
                  Sites = e.Locations.Select(x => x.Name).ToList(),
                  PurchaseOrderNumbers= e.PurchaseOrderNumbers
              });

方案2:处理JSON类型字段的特殊过滤

如果PurchaseOrderNumbers对应数据库的JSON数组字段(如SQL Server的NVARCHAR(MAX)存储JSON数组、PostgreSQL的jsonb),需使用EF Core的JSON查询函数:

SQL Server 示例

// 注意:需使用原始的JSON字符串属性,而非IEnumerable<string>
ratings = ratings.Where(e => EF.Functions.Like(e.PurchaseOrderNumbersJson, $"%\"{purchaseOrderFilter}\"%"));

PostgreSQL 示例

// 注意:需使用原始的JSON字符串属性,而非IEnumerable<string>
ratings = ratings.Where(e => EF.Functions.JsonContains(e.PurchaseOrderNumbersJson, $"[\"{purchaseOrderFilter}\"]"));

方案3:内存中过滤(仅小数据量场景)

如果数据集规模很小,可以先将查询结果加载到内存,再执行过滤:

var ratingsList = repository.GetAll<Rating>()
              .AsNoTracking()
              .Select(e => new RatingGridDto()
              {
                  Id = e.Id,
                  Sites = e.Locations.Select(x => x.Name).ToList(),
                  PurchaseOrderNumbers= e.PurchaseOrderNumbers
              })
              .ToList(); // 加载到内存
var filteredRatings = ratingsList.Where(e => e.PurchaseOrderNumbers.Any(pon => pon.Contains(purchaseOrderFilter))).AsQueryable();

自定义Expression方法的修正

你的自定义方法使用了Enumerable.Contains(内存方法),EF Core无法转换为SQL。需改为使用Queryable.Contains,并确保属性可被识别为数据库查询的IQueryable:

public static IQueryable<T> OwnWhereIsContainedBy<T, K>(IQueryable<T> source, K elementToContain, string propertyName)
{
    if (string.IsNullOrEmpty(propertyName))
    {
        throw new ArgumentException("Property name cannot be null or empty.", nameof(propertyName));
    }

    ParameterExpression parameter = Expression.Parameter(typeof(T));
    MemberExpression property = Expression.Property(parameter, propertyName);
    // 将属性转换为IQueryable
    var asQueryableCall = Expression.Call(typeof(Queryable), "AsQueryable", new[] { typeof(K) }, property);
    ConstantExpression element = Expression.Constant(elementToContain);

    // 使用Queryable的Contains方法
    MethodInfo containsMethod = typeof(Queryable).GetMethods()
        .Where(m => m.Name == "Contains" && m.GetParameters().Length == 2)
        .Single()
        .MakeGenericMethod(typeof(K));

    MethodCallExpression containsCall = Expression.Call(containsMethod, asQueryableCall, element);

    Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(containsCall, parameter);

    return source.Where(lambda);
}

注意:该修正仅适用于EF Core可识别为数据库查询的IQueryable属性,若PurchaseOrderNumbers是纯内存IEnumerable,依然无法转换。

内容的提问来源于stack exchange,提问作者ruddnisrus

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 03:47:16