为啥运行时表达式会导致 Entity Framework Core 5 的缓存发生冲突?
Posted
技术标签:
【中文标题】为啥运行时表达式会导致 Entity Framework Core 5 的缓存发生冲突?【英文标题】:Why runtime Expressions cause collisions on the Cache of Entity Framework Core 5?为什么运行时表达式会导致 Entity Framework Core 5 的缓存发生冲突? 【发布时间】:2021-10-14 12:06:28 【问题描述】:在我忘记它之前,我的执行上下文,我正在使用带有包的 .Net 5:
Microsoft.EntityFrameworkCore.Design 5.0.6 Microsoft.EntityFrameworkCore.Relational 5.0.6 mysql.EntityFrameworkCore 5.0.3.1我的主要目标是在需要检索实体时消除重复执行表达式的任务,例如:
public class GetListEntity
property int QueryProperty get; set
public class Entity
property int Property get; set
public async Task<ActionResult> List(GetListEntity getListEntity)
var restrictions = new List<Expression<Func<Entity>
if (model.QueryProperty != null)
restrictions.Add(e => e.Property == model.QueryProperty);
nonTrackedQueryableEntities = this.dbContext.Set<Entity>()
.AsNoTracking();
var expectedEntity = restrictions.Aggregate((sr, nr) => sr.And(nr)); //The And method is below as an extension
var expectedNonTrackedQueryableEntities = nonTrackedQueryableEntities.Where(expectedEntity);
// I will get the total first because the API was meant to paginate the responses.
var total = await expectedNonTrackedQueryableEntities.CountAsync();
public static class ExpressionExtensions
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> selfExpression, Expression<Func<T, bool>> otherExpression)
return selfExpression.Compose(otherExpression, Expression.OrElse);
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> selfExpression, Expression<Func<T, bool>> otherExpression)
return selfExpression.Compose(otherExpression, Expression.AndAlso);
private static InvocationExpression Casting<T>(this Expression<Func<T, bool>> selfExpression, Expression<Func<T, bool>> otherExpression)
return Expression.Invoke(otherExpression, selfExpression.Parameters.Cast<Expression>());
private static Expression<Func<T, bool>> Compose<T>(this Expression<Func<T, bool>> selfExpression, Expression<Func<T, bool>> otherExpression, Func<Expression, Expression, Expression> merge)
var invocationExpression = selfExpression.Casting(otherExpression);
return Expression.Lambda<Func<T, bool>>(merge(selfExpression.Body, invocationExpression), selfExpression.Parameters);
我已经设法实现了我想要的,但可以说......部分,因为如果我尝试连续至少两次查询数据库,我会得到这个异常:
System.ArgumentException: An item with the same key has already been added. Key: e
at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareLambda(LambdaExpression a, LambdaExpression b)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.Compare(Expression left, Expression right)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.Compare(Expression left, Expression right)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareBinary(BinaryExpression a, BinaryExpression b)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.Compare(Expression left, Expression right)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareLambda(LambdaExpression a, LambdaExpression b)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.Compare(Expression left, Expression right)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareUnary(UnaryExpression a, UnaryExpression b)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.Compare(Expression left, Expression right)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareExpressionList(IReadOnlyList`1 a, IReadOnlyList`1 b)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareMethodCall(MethodCallExpression a, MethodCallExpression b)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.Compare(Expression left, Expression right)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareExpressionList(IReadOnlyList`1 a, IReadOnlyList`1 b)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareMethodCall(MethodCallExpression a, MethodCallExpression b)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.Compare(Expression left, Expression right)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareExpressionList(IReadOnlyList`1 a, IReadOnlyList`1 b)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareMethodCall(MethodCallExpression a, MethodCallExpression b)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.Compare(Expression left, Expression right)
at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.Equals(Expression x, Expression y)
at Microsoft.EntityFrameworkCore.Query.CompiledQueryCacheKeyGenerator.CompiledQueryCacheKey.Equals(CompiledQueryCacheKey other)
at Microsoft.EntityFrameworkCore.Query.RelationalCompiledQueryCacheKeyGenerator.RelationalCompiledQueryCacheKey.Equals(RelationalCompiledQueryCacheKey other)
at MySql.EntityFrameworkCore.Query.Internal.MySQLCompiledQueryCacheKeyGenerator.MySQLCompiledQueryCacheKey.Equals(MySQLCompiledQueryCacheKey other)
at MySql.EntityFrameworkCore.Query.Internal.MySQLCompiledQueryCacheKeyGenerator.MySQLCompiledQueryCacheKey.Equals(Object obj)
at System.Collections.Concurrent.ConcurrentDictionary`2.TryGetValue(TKey key, TValue& value)
at Microsoft.Extensions.Caching.Memory.MemoryCache.TryGetValue(Object key, Object& result)
at Microsoft.Extensions.Caching.Memory.CacheExtensions.TryGetValue[TItem](IMemoryCache cache, Object key, TItem& value)
at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, Expression expression, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.CountAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)'
跟踪跟踪后,我设法发现 ORM 出于某种原因正在缓存我的表达式(并放置参数名称,在本例中为“e”),并且在第二次具有类似表达式时未能检测到键冲突查询数据库。我之所以这么说是因为,这不是主要交易,但至少很奇怪,缓存涉及非跟踪查询,也许我在中间遗漏了一些东西。
为了了解我是如何到达这里的,我将把代码放在下面。
首先在与查询实体列表相关的每个模型中实现一个接口,并公开扩展方法 ListRestrictions(几乎在底部)。
public interface IEntityFilter<TEntity>
下一步是定义 Attributes 以总结对属性执行的操作并生成部分表达式以在扩展方法中使用:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public abstract class FilterByPropertyAttribute : Attribute
protected string FirstPropertyPath get;
protected IEnumerable<string> NPropertyPath get;
public FilterByPropertyAttribute(string firstPropertyPath, params string[] nPropertyPath)
this.FirstPropertyPath = firstPropertyPath;
this.NPropertyPath = nPropertyPath;
protected MemberExpression GetPropertyExpression(ParameterExpression parameterExpression)
var propertyExpression = Expression.Property(parameterExpression, this.FirstPropertyPath);
foreach (var propertyPath in this.NPropertyPath)
propertyExpression = Expression.Property(propertyExpression, propertyPath);
return propertyExpression;
public abstract Expression GetExpression(ParameterExpression parameterExpression, object propertyValue);
为了避免与可为空的结构进行比较
public abstract class NonNullableValuePropertyFilterAttribute : FilterByPropertyAttribute
public NonNullableValuePropertyFilterAttribute(string firstPropertyPath, params string[] nPropertyPath)
: base(firstPropertyPath, nPropertyPath)
public override Expression GetExpression(ParameterExpression parameterExpression, object propertyValue)
var propertyExpression = this.GetPropertyExpression(parameterExpression);
return this.GetExpression(propertyExpression, this.GetConvertedConstantExpression(propertyExpression, Expression.Constant(propertyValue)));
protected abstract Expression GetExpression(MemberExpression memberExpression, UnaryExpression unaryExpression);
private UnaryExpression GetConvertedConstantExpression(MemberExpression memberExpression, ConstantExpression constantExpression)
var convertedConstantExpression = Expression.Convert(constantExpression, memberExpression.Type);
return convertedConstantExpression;
具有已定义角色的属性将是:
public class EqualPropertyFilterAttribute : NonNullableValuePropertyFilterAttribute
public EqualPropertyFilterAttribute(string firstPropertyPath, params string[] nPropertyPath)
: base(firstPropertyPath, nPropertyPath)
protected override Expression GetExpression(MemberExpression memberExpression, UnaryExpression unaryExpression)
return Expression.Equal(memberExpression, unaryExpression);
最后,扩展本身:
public static class EntityFilterExtensions
public static List<Expression<Func<TEntity, bool>>> ListRestrictions<TEntity>(this IEntityFilter<TEntity> entityFilter)
var entityFilterType = entityFilter.GetType();
var propertiesInfo = entityFilterType.GetProperties()
.Where(pi => pi.GetValue(entityFilter) != null
&& pi.CustomAttributes.Any(ca => ca.AttributeType
.IsSubclassOf(typeof(FilterByPropertyAttribute))));
var expressions = Enumerable.Empty<Expression<Func<TEntity, bool>>>();
if (propertiesInfo.Any())
var entityType = typeof(TEntity);
var parameterExpression = Expression.Parameter(entityType, "e");
expressions = propertiesInfo.Select(pi =>
var filterByPropertyAttribute = Attribute.GetCustomAttribute(pi, typeof(FilterByPropertyAttribute)) as FilterByPropertyAttribute;
var propertyValue = pi.GetValue(entityFilter);
var expression = filterByPropertyAttribute.GetExpression(parameterExpression, propertyValue);
return Expression.Lambda<Func<TEntity, bool>>(expression, parameterExpression);
);
return expressions.ToList();
一种用法是:
public class GetListEntity : IEntityFilter<Entity>
[EqualPropertyFilter(nameof(Entity.Property))]
property int QueryProperty get; set
public class Entity
property int Property get; set
public async Task<ActionResult> List(GetListEntity getListEntity)
var restrictions = getListEntity.ListRestrictions();
nonTrackedQueryableEntities = this.dbContext.Set<Entity>()
.AsNoTracking();
var expectedEntity = restrictions.Aggregate((sr, nr) => sr.And(nr));
var expectedNonTrackedQueryableEntities = nonTrackedQueryableEntities .Where(expectedEntity);
// I will get the total first because the API was meant to paginate the responses.
var total = await expectedNonTrackedQueryableEntities.CountAsync();
并且要被丢弃,如果我聚合表达式列表的非动态表达式,ORM 工作正常,当我使用动态表达式时,我一开始就会遇到异常。
我找到了一种解决方法,在扩展方法中更改了这一行:
var parameterExpression = Expression.Parameter(entityType, "e");
对于这个:
var parameterExpression = Expression.Parameter(entityType, $"entityType.NameentityFilter.GetHashCode()");
我想知道为什么会发生这种情况,也许还有其他方法可以解决它。 我在任何 Github 存储库中打开线程之前在这里发布了帖子,因为我仍然很好奇是否是我的错在路上遗漏了一些东西或一个错误。
【问题讨论】:
你用 Pomelo provider 试过了吗? 首先你确保提供repro(如果你去GitHub,他们会问同样的问题)因为我不能用这段代码重现。罪魁祸首可能在您未显示的代码中 - 标有 “添加所有可查询属性并在变量 expectedEntity 上聚合表达式” 的代码。其次(不相关),最好不要绑定常量表达式,而是绑定过滤器对象的属性,从而模拟闭包并允许 EF Core 将其映射到 db 参数。 @SvyatoslavDanyliv 是的,我试过了,得到了同样的错误,然后我尝试不使用这个表达式进行查询,它再次像一个魅力一样工作。但它会让我从原版代码开始。 @IvanStoev 1) 太好了,我会上传删除评论并添加代码的信息,我没有上传,因为我觉得解释太长了。 2)我没有把你带到那里,我正在尝试制作像 e => e.Property == 1 这样的表达式,从字面上使表达式像原始 e => e.Property == 模型有什么好处.查询属性?我一点也不抱怨,但我看到的是我需要再次使用模型对象来检索属性,目的是避免在我列出限制时获得值时再次调用该对象。 @NoName Re: (2) 好处是 EF Core 将创建参数化查询,例如... WHERE [table].[column] = @param
,这通常更好,因为 db 查询优化器可以缓存和重用查询计划。使用嵌入在 SQL 中的文字值,他们必须每次都重新编译 SQL 查询
【参考方案1】:
从解释中可以清楚地看出,动态构建的谓词的ParameterExpression
s 存在一些问题。最后是使用的自定义表达式扩展方法之一。
虽然从技术上讲它可能被认为是 ORM 错误/问题,但他们必须在表达式树转换期间解决非常复杂的事情,因此我们必须容忍并尽可能修复我们的代码。
在构建动态查询表达式树时需要注意一些重要事项。
首先,使用的ParameterExpression
s 的名称无关紧要 - 它们由reference标识。只要它们是由其他表达式正确引用的单独实例,就可以让所有参数具有同一个名称(C# 编译器不允许您在编译时创建的名称)。
其次,在创建表达式树以作为代码编译和执行时(例如在 LINQ to Objects 中),一些有意义的事情对于应该被转换为其他东西的表达式树并不好(它们是有效的,但是使转换更加困难并导致错误/问题)。具体来说(导致问题的原因)是“调用” lambda 表达式。是的,有一个专用的Expression.Invoke
,但它导致了几乎所有IQueryable
实现的问题,所以最好通过“内联”它来模拟它,这意味着用实际表达式替换正文中的参数实例。
这是您的 ExpressionExtensions
类应用上述原则的修改版本:
public static partial class ExpressionExtensions
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
=> Combine(left, right, ExpressionType.AndAlso);
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
=> Combine(left, right, ExpressionType.OrElse);
private static Expression<Func<T, bool>> Combine<T>(Expression<Func<T, bool>> left, Expression<Func<T, bool>> right, ExpressionType type)
if (left is null) return right;
if (right is null) return left;
bool constValue = type == ExpressionType.AndAlso ? false : true;
if ((left.Body as ConstantExpression)?.Value is bool leftValue)
return leftValue == constValue ? left : right;
if ((right.Body as ConstantExpression)?.Value is bool rightValue)
return rightValue == constValue ? right : left;
return Expression.Lambda<Func<T, bool>>(Expression.MakeBinary(type,
left.Body, right.Invoke(left.Parameters[0])),
left.Parameters);
public static Expression Invoke<T, TResult>(this Expression<Func<T, TResult>> source, Expression arg)
=> source.Body.ReplaceParameter(source.Parameters[0], arg);
它使用以下小助手进行参数替换:
public static partial class ExpressionExtensions
public static Expression ReplaceParameter(this Expression source, ParameterExpression parameter, Expression value)
=> new ParameterReplacer Parameter = parameter, Value = value .Visit(source);
class ParameterReplacer : ExpressionVisitor
public ParameterExpression Parameter;
public Expression Value;
protected override Expression VisitParameter(ParameterExpression node)
=> node == Parameter ? Value : node;
正如 cmets 所证实的,这解决了问题。
现在,无关紧要,但作为奖励。对应该编译的表达式有意义的另一件事是使用 ConstantExpression
s - 它们被评估一次,然后在可能的许多地方使用。
但是对于应该转换为 SQL 或类似的表达式树,使用ConstantExpression
s 会使每个查询不同,因此不可缓存。出于性能原因,最好使用被视为 variable 的表达式类型,从而允许缓存转换和参数化生成的 SQL 查询,因此客户端和数据库查询处理器都可以重用“已编译”查询/执行计划。
这样做很容易。它不需要更改谓词的类型或生成方式。您只需将ConstantExpression
替换为ConstantExpression
的member(属性/字段)即可。在您的情况下,这是更换的问题
var propertyValue = pi.GetValue(entityFilter);
与
var propertyValue = Expression.Property(Expression.Constant(entityFilter), pi);
当然还有调整签名/实现(如果它们对于方法不是必需的,通常尽量不使用特定的表达式类型),例如
FilterByPropertyAttribute 类:
public abstract Expression GetExpression(ParameterExpression parameter, Expression value);
NonNullableValuePropertyFilterAttribute 类:
public override Expression GetExpression(ParameterExpression parameter, Expression value)
var property = this.GetPropertyExpression(parameter);
if (value.Type != property.Type)
value = Expression.Convert(value, property.Type);
return this.GetExpression(property, value);
protected abstract Expression GetExpression(MemberExpression member, Expression value);
EqualPropertyFilterAttribute 类:
protected override Expression GetExpression(MemberExpression member, Expression value)
=> Expression.Equal(member, value);
所有其他内容,包括用法保持不变。但结果将是很好的参数化查询,就好像它是在编译时创建的一样。
【讨论】:
令人惊讶的 Ivan,这真的帮助了我,而且我看到你甚至花时间简化了And
和 Or
的定义,将想要的 Type
委托给 MakeBinary
方法。而对于额外的部分,我真的很想不通,这真的比我预期的要容易。无论如何,谢谢你帮助我!以上是关于为啥运行时表达式会导致 Entity Framework Core 5 的缓存发生冲突?的主要内容,如果未能解决你的问题,请参考以下文章
当 SQL 在 SQL 选项卡中正常运行时,为啥创建此视图会导致错误 1350?
为啥将 <%= %> 表达式作为服务器控件上的属性值会导致编译错误?
JavaScript - 为啥包含括号会导致三元表达式错误?
为啥 Django makemigrations 每次运行时都会检测到由于 help_text/verbose_name 属性中的重音而导致的更改?