为啥运行时表达式会导致 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】:

从解释中可以清楚地看出,动态构建的谓词的ParameterExpressions 存在一些问题。最后是使用的自定义表达式扩展方法之一。

虽然从技术上讲它可能被认为是 ORM 错误/问题,但他们必须在表达式树转换期间解决非常复杂的事情,因此我们必须容忍并尽可能修复我们的代码。

在构建动态查询表达式树时需要注意一些重要事项。

首先,使用的ParameterExpressions 的名称无关紧要 - 它们由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 所证实的,这解决了问题。


现在,无关紧要,但作为奖励。对应该编译的表达式有意义的另一件事是使用 ConstantExpressions - 它们被评估一次,然后在可能的许多地方使用。

但是对于应该转换为 SQL 或类似的表达式树,使用ConstantExpressions 会使每个查询不同,因此不可缓存。出于性能原因,最好使用被视为 variable 的表达式类型,从而允许缓存转换和参数化生成的 SQL 查询,因此客户端和数据库查询处理器都可以重用“已编译”查询/执行计划。

这样做很容易。它不需要更改谓词的类型或生成方式。您只需将ConstantExpression 替换为ConstantExpressionmember(属性/字段)即可。在您的情况下,这是更换的问题

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,这真的帮助了我,而且我看到你甚至花时间简化了 AndOr 的定义,将想要的 Type 委托给 MakeBinary方法。而对于额外的部分,我真的很想不通,这真的比我预期的要容易。无论如何,谢谢你帮助我!

以上是关于为啥运行时表达式会导致 Entity Framework Core 5 的缓存发生冲突?的主要内容,如果未能解决你的问题,请参考以下文章

当 SQL 在 SQL 选项卡中正常运行时,为啥创建此视图会导致错误 1350?

为啥将 <%= %> 表达式作为服务器控件上的属性值会导致编译错误?

JavaScript - 为啥包含括号会导致三元表达式错误?

为啥这个表达式会导致浮点错误?

为啥 Django makemigrations 每次运行时都会检测到由于 help_text/verbose_name 属性中的重音而导致的更改?

为啥类中的 stringstream 成员会导致编译时错误? [复制]