如何为Expression<Func<T,bool>>与Include委托生成唯一缓存键?
解决方案
一、处理Expression<Func<T, bool>>的唯一标识
直接用filter.Body.ToString()会出现参数名不一致的问题(比如x=>x.Id==1和y=>y.Id==1会生成不同字符串),而GetHashCode()因为实例动态创建会返回不同值。可以通过以下方式解决:
- 标准化表达式树:自定义
ExpressionVisitor将表达式中的参数替换为统一名称,确保逻辑相同的表达式生成一致的字符串。 - 生成稳定标识:将标准化后的表达式转换为字符串,作为唯一标识。
示例代码:
public static string GetExpressionKey<T>(Expression<Func<T, bool>> expression) { var normalizedExpr = new ParameterNormalizerVisitor().Visit(expression); return normalizedExpr.ToString(); } private class ParameterNormalizerVisitor : ExpressionVisitor { private readonly Dictionary<ParameterExpression, ParameterExpression> _parameterMap = new(); protected override Expression VisitParameter(ParameterExpression node) { if (!_parameterMap.TryGetValue(node, out var replacement)) { replacement = Expression.Parameter(node.Type, "p"); _parameterMap[node] = replacement; } return replacement; } }
二、处理Include委托的唯一标识
Include委托本质是对查询添加Include/ThenInclude调用,我们需要解析出其中包含的导航属性路径,以此作为唯一标识:
- 解析Include调用:通过反射获取EF Core查询对象中存储的Include路径信息。
- 拼接路径字符串:将所有导航属性路径拼接成统一格式的字符串。
示例代码(适配EF Core):
public static string GetIncludesKey<T>(Func<IQueryable<T>, IIncludableQueryable<T, object>> includes) { if (includes == null) return string.Empty; var query = includes(Queryable.Empty<T>()); var includePaths = query.GetIncludes().Select(i => string.Join(".", i.Path.Split('.').Select(p => p.Trim()))); return string.Join(",", includePaths); } // 扩展方法提取Include路径 public static IEnumerable<IncludeQuerySource> GetIncludes<T>(this IQueryable<T> query) { var enumerable = query as IIncludableQueryable<T, object>; if (enumerable == null) yield break; var includeMetadata = enumerable.GetType().GetProperty("Includes", BindingFlags.NonPublic | BindingFlags.Instance)? .GetValue(enumerable) as IEnumerable; if (includeMetadata == null) yield break; foreach (var item in includeMetadata) { var pathProp = item.GetType().GetProperty("Path", BindingFlags.Public | BindingFlags.Instance); if (pathProp != null) { yield return new IncludeQuerySource { Path = pathProp.GetValue(item)?.ToString() ?? string.Empty }; } } } public class IncludeQuerySource { public string Path { get; set; } }
三、修改缓存键生成函数
将上述两个方法整合到GetKey中,生成稳定的缓存键:
private string GetKey(string id, Func<IQueryable<Session>, IIncludableQueryable<Session, object>> includes = null, Expression<Func<Session, bool>> filter = null, bool trackChanges = true) { StringBuilder sb = new StringBuilder(); sb.Append("Session:"); sb.Append(trackChanges ? 1 : 0); sb.Append(":"); sb.Append(id); if (includes != null) { sb.Append(":includes="); sb.Append(GetIncludesKey(includes)); } if (filter != null) { sb.Append(":filter="); sb.Append(GetExpressionKey(filter)); } return sb.ToString(); }
额外注意事项
- 性能优化:如果频繁生成缓存键,可以将表达式和Include路径的标识结果做本地缓存,避免重复解析。
- 缩短键长度:如果担心字符串过长,可以将标准化后的字符串计算MD5/SHA256哈希,用哈希值作为标识。
- ORM兼容性:解析Include的方法依赖EF Core内部实现,若使用其他ORM,需对应调整反射逻辑。
内容的提问来源于stack exchange,提问作者greyhame
相关产品推荐
相关产品推荐

