针对单个关联实体,如何复用表达式实现查询过滤?
EF Core关联实体过滤:一对多反向查询的表达式复用问题
背景
我首次尝试用表达式减少代码重复,针对两个关联实体分别创建了表达式过滤函数:
CMSite(站点)CMSiteServer(站点服务器)
实体定义如下:
public class CMSite { public int ID {get;set;} public string Name {get;set;} public ICollection<CMSiteServer> SiteServers { get; set; } } public class CMSiteServer { public int ID {get;set;} public string Name {get;set;} public int SiteID {get;set;} [ForeignKey("CMSite")] public decimal SiteID { get; set; } // 注:此处存在重复定义,建议修正为统一类型 public CMSite Site {get;set;} }
两者为一对多关系:1个CMSite对应多个CMSiteServer。我已经为每个实体创建了过滤对象,以及对应的表达式生成方法:
public static Expression<Func<CMSiteServer, bool>> GetServerExpression(CMSiteServerFilter filter, bool Active) { // 实现逻辑:根据filter生成CMSiteServer的过滤表达式 } public static Expression<Func<CMSite, bool>> GetSiteExpression(CMSiteFilter filter, bool Active) { // 实现逻辑:根据filter生成CMSite的过滤表达式 }
用例与问题
需要实现两个查询场景:
- 搜索包含符合条件服务器的站点(已实现)
- 搜索属于符合条件站点的服务器(未实现)
已实现的用例1代码
public static IQueryable<CMSite> ApplyQueryFilterServer(this IQueryable<CMSite> qry, CMSiteServerFilter filter, bool Active) { if (filter == null) return qry; var exp = GetServerExpression(filter, Active); qry = qry.Where(s => s.SiteServers.AsQueryable().Any(exp)); return qry; }
未实现的用例2代码(存在问题)
// 方法返回值错误,应该是IQueryable<CMSiteServer>而非IQueryable<CMSite> public static IQueryable<CMSite> ApplyQueryFilterSite(this IQueryable<CMSiteServer> qry, CMSiteFilter filter, bool Active) { if (filter == null) return qry; var exp = GetServerExpression(filter, Active); // 此处误用了服务器的表达式,应该用站点的表达式 // 错误原因:s.CMSite是单个实体,无法调用AsQueryable()和Any() qry = qry.Where((s) => s.CMSite.AsQueryable().Any(exp)); return qry; }
解决方案
核心思路:复用已有的GetSiteExpression生成的站点过滤表达式,将其转换为针对CMSiteServer的过滤逻辑——因为每个CMSiteServer关联单个CMSite,只需判断该关联的CMSite是否满足过滤条件即可。
正确实现方式(支持EF Core SQL翻译)
避免使用Compile()(会导致客户端评估,影响性能),而是通过表达式树拼接将站点表达式转换为服务器的过滤表达式:
public static IQueryable<CMSiteServer> ApplyQueryFilterSite(this IQueryable<CMSiteServer> qry, CMSiteFilter filter, bool Active) { if (filter == null) return qry; // 获取针对CMSite的过滤表达式 var siteFilterExp = GetSiteExpression(filter, Active); // 构建针对CMSiteServer的表达式:参数为s(CMSiteServer),判断s.Site是否满足siteFilterExp var parameter = Expression.Parameter(typeof(CMSiteServer), "s"); // 获取s.Site属性的表达式 var siteProperty = Expression.Property(parameter, nameof(CMSiteServer.Site)); // 调用siteFilterExp表达式,传入s.Site作为参数 var filterBody = Expression.Invoke(siteFilterExp, siteProperty); // 生成最终的Lambda表达式:Func<CMSiteServer, bool> var serverFilterExp = Expression.Lambda<Func<CMSiteServer, bool>>(filterBody, parameter); // 应用过滤条件 qry = qry.Where(serverFilterExp); return qry; }
简化版(适合简单场景,注意客户端评估风险)
如果不需要EF Core将整个查询转换为SQL(比如数据量小),可以直接编译表达式后调用:
public static IQueryable<CMSiteServer> ApplyQueryFilterSite(this IQueryable<CMSiteServer> qry, CMSiteFilter filter, bool Active) { if (filter == null) return qry; var siteFilter = GetSiteExpression(filter, Active).Compile(); qry = qry.Where(s => siteFilter(s.Site)); return qry; }
关键说明
- 修正了原方法的返回值错误:输入是
IQueryable<CMSiteServer>,返回值应该对应为IQueryable<CMSiteServer>而非IQueryable<CMSite> - 复用已有的
GetSiteExpression,避免重复编写站点过滤逻辑 - 表达式树拼接的方式能让EF Core正确生成SQL查询,避免客户端评估带来的性能问题
内容的提问来源于stack exchange,提问作者Justin
相关产品推荐
相关产品推荐

