Store Static Filter By Key Expression

自作多情 提交于 2019-12-06 10:12:18

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!