如何为Entity Framework创建支持ICollection的动态通用Where子句?
问题描述
尝试为Entity Framework创建运行时动态生成Where子句的代码,处理直接对象属性正常,但处理集合属性(如Parent类的Children)时抛出错误:
System.ArgumentException: 'Instance property 'Name' is not defined for type 'System.Collections.Generic.ICollection`1[Child]' (Parameter 'propertyName')'
实体类代码
public class Parent { public int Id { get; set; } public string Name { get; set; } public Child Child { get; set; } // 此属性处理正常 public ICollection<Child> Children { get; set; } // 集合属性处理失败 } public class Child { public int Id { get; set; } public string Name { get; set; } public int ParentId { get; set; } public Parent Parent { get; set; } }
调用及动态Where方法代码
public static void Main() { using (var context = new MyDbContext()) { var filters = new Dictionary<string, object> { { "Name", "Parent1" }, { "Child.Name","Child2" }, // 正常工作 { "Children.Name", "Child1" } // 无法工作 }; var results = ApplyDynamicWhereClause<Parent>(context.Parents, filters).Include(p => p.Children).ToList(); foreach (var parent in results) { Console.WriteLine($"{parent.Name}:"); foreach (var child in parent.Children) { Console.WriteLine($" {child.Name}"); } } } } public IQueryable<T> ApplyDynamicWhereClause<T>(IQueryable<T> query, Dictionary<string, object> filters) { var parameter = Expression.Parameter(typeof(T), "x"); Expression predicate = Expression.Constant(true); foreach (var filter in filters) { string[] propertyPath = filter.Key.Split('.'); Expression property = parameter; Type currentType = typeof(T); foreach (var propertyName in propertyPath) { property = Expression.Property(property, propertyName); currentType = currentType.GetProperty(propertyName).PropertyType; } var constant = Expression.Constant(filter.Value); var equals = Expression.Equal(property, constant); predicate = Expression.AndAlso(predicate, equals); } var lambda = Expression.Lambda<Func<T, bool>>(predicate, parameter); return query.Where(lambda); }
Mock DbContext代码
public class MyDbContext : DbContext { public DbSet<Parent> Parents { get; set; } public DbSet<Child> Children { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { //optionsBuilder.UseInMemoryDatabase("TestDb"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Parent>().HasData( new Parent { Id = 1, Name = "Parent1" }, new Parent { Id = 2, Name = "Parent2" }); modelBuilder.Entity<Child>().HasData( new Child { Id = 1, Name = "Child1", ParentId = 1 }, new Child { Id = 2, Name = "Child2", ParentId = 1 }, new Child { Id = 3, Name = "Child3", ParentId = 2 }); } }
解决方案
问题核心是:ICollection<Child>本身没有Name属性,你需要的是判断集合是否存在满足条件的元素,而非直接访问集合的属性。修改ApplyDynamicWhereClause方法,在遍历属性路径时检测集合类型,生成Any()表达式来实现集合条件过滤。
修改后的方法代码:
public static IQueryable<T> ApplyDynamicWhereClause<T>(IQueryable<T> query, Dictionary<string, object> filters) { var parameter = Expression.Parameter(typeof(T), "x"); Expression predicate = Expression.Constant(true); foreach (var filter in filters) { string[] propertyPath = filter.Key.Split('.'); Expression property = parameter; Type currentType = typeof(T); for (int i = 0; i < propertyPath.Length; i++) { var propertyName = propertyPath[i]; var prop = currentType.GetProperty(propertyName); property = Expression.Property(property, prop); currentType = prop.PropertyType; // 检测是否为非字符串的集合类型 if (i < propertyPath.Length - 1 && typeof(System.Collections.IEnumerable).IsAssignableFrom(currentType) && currentType != typeof(string)) { var elementType = currentType.GetGenericArguments()[0]; var elementParam = Expression.Parameter(elementType, "item"); // 构建集合的Any()调用,传入元素条件lambda property = Expression.Call( typeof(Enumerable), "Any", new[] { elementType }, property, Expression.Lambda(Expression.Property(elementParam, propertyPath[i+1]), elementParam) ); i++; // 跳过下一个属性,已在Any中处理 currentType = typeof(bool); // Any返回bool类型 } } var constant = Expression.Constant(filter.Value); var equals = Expression.Equal(property, constant); predicate = Expression.AndAlso(predicate, equals); } var lambda = Expression.Lambda<Func<T, bool>>(predicate, parameter); return query.Where(lambda); }
代码说明
- 集合检测逻辑:判断当前属性是否为
IEnumerable(排除字符串),若后续还有属性路径,则生成Any()调用。 - Any表达式构建:通过
Enumerable.Any静态方法,传入集合表达式和检测元素条件的lambda,实现"集合中存在满足条件的元素"的逻辑。 - 路径跳过处理:处理完集合后跳过下一个属性名,避免重复解析。
修改后,"Children.Name": "Child1"会被解析为x.Children.Any(item => item.Name == "Child1"),符合EF Core查询语法,可正常过滤包含指定子元素的父实体。
内容的提问来源于stack exchange,提问作者Minhaj
相关产品推荐
相关产品推荐

