c# - 按键表达式存储静态过滤器

标签 c# entity-framework .net-core linq-expressions query-cache

我有一个函数,它生成一个表达式来通过它的主键过滤表,当传入 Object[] 时,这与 Find 非常相似函数,只是它没有实现,所以你可以在之后传递一个 IQueryable

public static Expression<Func<T, bool>> FilterByPrimaryKeyPredicate<T>(this DbContext dbContext, object[] id)
{
    var keyProperties = dbContext.GetPrimaryKeyProperties<T>();
    var parameter = Expression.Parameter(typeof(T), "e");
    var body = keyProperties
        // e => e.{propertyName} == new {id = id[i]}.id
        .Select((p, i) => Expression.Equal(
            Expression.Property(parameter, p.Name),
            Expression.Convert(
                Expression.PropertyOrField(Expression.Constant(new { id = id[i] }), "id"),
                p.ClrType)))
        .Aggregate(Expression.AndAlso);
    return Expression.Lambda<Func<T, bool>>(body, parameter);
}

这通过首先获取表的主键来工作,它为每个属性创建二进制表达式,Id 包装在匿名类型中以利用查询缓存。这工作正常。但是,我想更进一步。

我想保留表达式,这样我就不必在每次传递一组新的 id 时都生成它,如何在仍然利用查询缓存的同时存储这个 Expression

编辑 TL;DR

所以我尝试按照建议在静态类中使用数组访问来缓存它,但是我遇到了一个错误:

public class PrimaryKeyFilterContainer<T>
{
    const string ANON_ID_PROP = "id";
    static Expression<Func<T, bool>> _filter;
    Type ANON_TYPE = new { id = (object)0 }.GetType();
    public object[] id { get; set; }

    public PrimaryKeyFilterContainer()
    {
    }

    public Expression<Func<T, bool>> GetFilter(DbContext dbContext, object[] id)
    {
        this.id = id;

        if(null == _filter)
        {
            var keyProperties = dbContext.GetPrimaryKeyProperties<T>();
            var parameter = Expression.Parameter(typeof(T), "e");
            var body = keyProperties
                // e => e.PK[i] == id[i]
                .Select((p, i) => Expression.Equal(
                    Expression.Property(parameter, p.Name),
                    Expression.Convert(BuildNewExpression(i),
                        p.ClrType)))
                .Aggregate(Expression.AndAlso);

            _filter = Expression.Lambda<Func<T, bool>>(body, parameter);
        }

        return _filter;
    }

    NewExpression BuildNewExpression(int index)
    {
        var currentObject = Expression.Constant(this);
        var fieldAccess = Expression.PropertyOrField(currentObject, nameof(id));
        var arrayAccess = Expression.ArrayAccess(fieldAccess, Expression.Constant(index));
        return Expression.New(ANON_TYPE.GetConstructor(new[] { typeof(object) }), arrayAccess);
    }
}

No coercion operator is defined between types '<>f__AnonymousType0`1[System.Object]' and 'System.Int32'

我越来越接近了,但我不确定它是否仍然有效。

最佳答案

正如我在评论中提到的,主要问题是我们不能在表达式树中使用数组索引访问 - EF6 抛出不受支持的异常,EF Core 将其转换为客户端评估。

所以我们需要将键存储在一个具有动态属性和属性类型计数的类中。幸运的是,System.Tuple 泛型类提供了此类功能,并且可以在 EF6 和 EF Core 中使用。

下面是实现上述思想的类:

public class PrimaryKeyFilter<TEntity>
    where TEntity : class
{
    object valueBuffer;
    Func<object[], object> valueArrayConverter;

    public PrimaryKeyFilter(DbContext dbContext)
    {
        var keyProperties = dbContext.GetPrimaryKeyProperties<TEntity>();

        // Create value buffer type (Tuple) from key properties
        var valueBufferType = TupleTypes[keyProperties.Count - 1]
            .MakeGenericType(keyProperties.Select(p => p.ClrType).ToArray());

        // Build the delegate for converting value array to value buffer
        {
            // object[] values => new Tuple(values[0], values[1], ...)
            var parameter = Expression.Parameter(typeof(object[]), "values");
            var body = Expression.New(
                valueBufferType.GetConstructors().Single(),
                keyProperties.Select((p, i) => Expression.Convert(
                    Expression.ArrayIndex(parameter, Expression.Constant(i)),
                    p.ClrType)));
            valueArrayConverter = Expression.Lambda<Func<object[], object>>(body, parameter).Compile();
        }

        // Build the predicate expression
        {
            var parameter = Expression.Parameter(typeof(TEntity), "e");
            var valueBuffer = Expression.Convert(
                Expression.Field(Expression.Constant(this), nameof(this.valueBuffer)),
                valueBufferType);
            var body = keyProperties
                // e => e.{propertyName} == valueBuffer.Item{i + 1}
                .Select((p, i) => Expression.Equal(
                    Expression.Property(parameter, p.Name),
                    Expression.Property(valueBuffer, $"Item{i + 1}")))
                .Aggregate(Expression.AndAlso);
            Predicate = Expression.Lambda<Func<TEntity, bool>>(body, parameter);
        }
    }

    public Expression<Func<TEntity, bool>> Predicate { get; }

    public void SetValues(params object[] values) =>
        valueBuffer = valueArrayConverter(values);

    static readonly Type[] TupleTypes =
    {
        typeof(Tuple<>),
        typeof(Tuple<,>),
        typeof(Tuple<,,>),
        typeof(Tuple<,,,>),
        typeof(Tuple<,,,,>),
        typeof(Tuple<,,,,,>),
        typeof(Tuple<,,,,,,>),
        typeof(Tuple<,,,,,,,>),
    };
}

您可以创建和存储类的实例。然后在查询中使用 Predicate 属性返回的表达式。和SetValues方法来设置参数。

缺点是值存储绑定(bind)到类实例,因此不能并发使用。原始方法在所有情况下都适用,IMO 对性能的影响应该可以忽略不计,因此您可以考虑继续使用。

关于c# - 按键表达式存储静态过滤器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51054890/

相关文章:

c# - 客户端验证错误

java - C#和Java的三元运算符的区别(?:)

c# - EFCore - 如何将多个导航属性设置为同一类型?

c# - ASP.NET MVC 代码优先 : Multiplicity is not valid in Role in relationship

c# - '$' 字符串前缀不用于插值时意味着什么?

c# - 如何在 .net 核心中使用 Openstreetmap(osm) 离线图 block 文件?

c# - 没有登录控件的 ASP.NET 登录表单例份验证

c# - 为什么在 oauth2 中缓存访问 token 被认为是不好的?

asp.net-mvc-3 - 如何在 ASP.NET MVC 3 中分离代码(干净的代码分离)?

c# - 反序列化之前使用 System.Text.Json 序列化的文件时出现异常