自定义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
相关产品推荐
相关产品推荐

