按关键字表达式存储静态过滤器 [英] Store Static Filter By Key Expression

查看:17
本文介绍了按关键字表达式存储静态过滤器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

public static Expression>FilterByPrimaryKeyPredicate(这个 DbContext dbContext,object[] id){var keyProperties = dbContext.GetPrimaryKeyProperties();var 参数 = 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.PropertyOrField(Expression.Constant(new { id = id[i] }), "id"),p.ClrType))).Aggregate(Expression.AndAlso);return Expression.Lambda(body, parameter);}

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

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

编辑 TL;DR

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

公共类 PrimaryKeyFilterContainer{const string ANON_ID_PROP = "id";静态表达式<Func<T,bool>_筛选;类型 ANON_TYPE = new { id = (object)0 }.GetType();公共对象 [] id { 获取;放;}公共 PrimaryKeyFilterContainer(){}公共表达式<Func<T, bool>>GetFilter(DbContext dbContext, object[] id){this.id = id;如果(空 == _filter){var keyProperties = dbContext.GetPrimaryKeyProperties();var 参数 = 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>(body, parameter);}返回_过滤器;}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);}}

<块引用>

类型'<>f__AnonymousType0`1[System.Object]'和'System.Int32'之间没有定义强制操作符

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

解决方案

正如我在评论中提到的,主要问题是我们无法在表达式树内部使用数组索引访问 - EF6 throws not supported exception 并且 EF Core 转了它进入客户评估.

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

以下是实现上述想法的类:

public class PrimaryKeyFilter其中 TEntity : 类{对象值缓冲区;Func<object[], object>值数组转换器;公共主键过滤器(DbContext dbContext){var keyProperties = dbContext.GetPrimaryKeyProperties();//从键属性创建值缓冲区类型(元组)var valueBufferType = TupleTypes[keyProperties.Count - 1].MakeGenericType(keyProperties.Select(p => p.ClrType).ToArray());//构建用于将值数组转换为值缓冲区的委托{//object[] values =>新元组(值[0],值[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>(body, parameter).Compile();}//构建谓词表达式{var 参数 = Expression.Parameter(typeof(TEntity), "e");var valueBuffer = Expression.Convert(Expression.Field(Expression.Constant(this), nameof(this.valueBuffer)),值缓冲区类型);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(body, parameter);}}公共表达式<Func<TEntity, bool>>谓词{得到;}public void SetValues(params object[] values) =>valueBuffer = valueArrayConverter(values);静态只读 Type[] TupleTypes ={typeof(Tuple>),typeof(Tuple<,>),typeof(Tuple<,,>),typeof(Tuple<,,,>),typeof(Tuple<,,,,>),typeof(Tuple<,,,,,>),typeof(Tuple<,,,,,,>),typeof(Tuple<,,,,,,,>),};}

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

缺点是值存储是绑定在类实例上的,不能并发使用.原始方法适用于所有场景,对 IMO 的性能影响应该可以忽略不计,因此您可以考虑继续使用.

I've got an function which generates an expression to filter a table by it's primary key, when passed in an Object[], this is very similar to Find function except that it doesn't materialize so you can pass an IQueryable around afterwards

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);
}

This works by first getting the primary keys for a table, it creates binary expression foreach property, the Id is wrapped in an anonymous type to leverage the query cache. This is working fine. However, I'd like to take this a step further.

I'd like to preserve the Expression so I don't have to generate it each time I pass on a new set of ids, How can I store this Expression while still leveraging the Query Cache?

Edit TL;DR

So I'm attempt to cache it using array access in a static class as suggest, however I'm encountering an error:

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'

I'm getting closer but I'm not sure if it's going to work still.

解决方案

As I mentioned in the comments, the main problem is that we cannot use array index access inside the expression tree - EF6 throws not supported exception and EF Core turns it into client evaluation.

So we need to store the keys in a class with dynamic count of properties and property types. Fortunately the System.Tuple generic classes provide such functionality, and can be used in both EF6 and EF Core.

Following is a class that implements the above idea:

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<,,,,,,,>),
    };
}

You can create and store an instance of the class. Then use the expression returned by the Predicate property inside the query. And SetValues method to set the parameters.

The drawback is that the value storage is bound to the class instance, hence it cannot be used concurrently. The original approach works well in all scenarios, and the performance impact IMO should be negligible, so you might consider staying on it.

这篇关于按关键字表达式存储静态过滤器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆