Pull to refresh

Устранение дублирования Where Expressions в приложении

Reading time 4 min
Views 21K
Допустим, у вас есть товары и категории. В какой-то момент клиент сообщает, что для категорий с рейтингом > 50 необходимо использовать другие бизнес-процессы. У вас достаточно опыта и вы понимаете, что где сегодня 50 завтра будет 127.37 и хотите избежать появления магических чисел в коде, поэтому делаете так:

    public class Category : HasIdBase<int>
    {
        public static readonly Expression<Func<Category, bool>> NiceRating = x => x.Rating > 50;

       //...
    }

    var niceCategories = db.Query<Category>.Where(Category.NiceRating);

К сожалению, этот номер не пройдет, если вы хотите выбрать продукты из соответствующих категорий, потому что NiceRating имеет тип Expression<Func<Category, bool>>, а в случае с Product нам потребуется Expression<Func<Product, bool>>. То есть, необходимо осуществить преобразование Expression<Func<Category, bool>> => Expression<Func<Product, bool>>.

    public class Product: HasIdBase<int>
    {
        public virtual Category Category { get; set; }

       //...
    }

    var niceProductsCompilationError = db.Query<Product>.Where(Category.NiceRating); // так нельзя!

К счастью, осуществить это довольно просто!

     // Фактически мы реализуем композицию выражений,
     // которая даст нам выражение, соответствующее композиции целевых функций
     public static Expression<Func<TIn, TOut>> Compose<TIn, TInOut, TOut>(
             this Expression<Func<TIn, TInOut>> input,
             Expression<Func<TInOut, TOut>> inOutOut)
        {
            // это параметр x => blah-blah. Для лямбды нам нужен null
            var param = Expression.Parameter(typeof(TIn), null);
            // получаем объект, к которому применяется выражение
            var invoke = Expression.Invoke(input, param);
            // и выполняем "получи объект и примени к нему его выражение"
            var res = Expression.Invoke(inOutOut, invoke);
            
            // возвращаем лямбду нужного типа
            return Expression.Lambda<Func<TIn, TOut>>(res, param);
        }
        
        // Добавляем "продвинутый" вариант Where
        public static IQueryable<T> Where<T, TParam>(this IQueryable<T> queryable,
            Expression<Func<T, TParam>> prop, Expression<Func<TParam, bool>> where)
        {
            return queryable.Where(prop.Compose(where));
        }
	
        // Проверяем
	[Fact]
	public void AdvancedWhere_Works()
	{
		var product = new Product(new Category() {Rating = 700}, "Some Product", 100500);
		var q = new[] {product}.AsQueryable();

		var values = q.Where(x => x.Category, Category.NiceRating).ToArray();
		Assert.Equal(700, values[0].Category.Rating);
	}

Так реализована композиция выражений в LinqKit. Однако, Entity Framework не дружит с InvokeExpression и выкидывает NotSupportedException. Вы же знали, что LINQ протекает? Чтобы обойти это ограничение в LinqKit используется хак метод расширения AsExpandable. Эту проблему описал Pete Montgomery в своем блоге. Его версия Predicate Builder не требует особой уличной магии и работает как для IEnumerable<T>, так и для IQueryable<T>.

Привожу код as is.
public static class PredicateBuilder
{
    /// <summary>
    /// Creates a predicate that evaluates to true.
    /// </summary>
    public static Expression<Func<T, bool>> True<T>() { return param => true; }
 
    /// <summary>
    /// Creates a predicate that evaluates to false.
    /// </summary>
    public static Expression<Func<T, bool>> False<T>() { return param => false; }
 
    /// <summary>
    /// Creates a predicate expression from the specified lambda expression.
    /// </summary>
    public static Expression<Func<T, bool>> Create<T>(Expression<Func<T, bool>> predicate) { return predicate; }
 
    /// <summary>
    /// Combines the first predicate with the second using the logical "and".
    /// </summary>
    public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
    {
        return first.Compose(second, Expression.AndAlso);
    }
 
    /// <summary>
    /// Combines the first predicate with the second using the logical "or".
    /// </summary>
    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
    {
        return first.Compose(second, Expression.OrElse);
    }
 
    /// <summary>
    /// Negates the predicate.
    /// </summary>
    public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> expression)
    {
        var negated = Expression.Not(expression.Body);
        return Expression.Lambda<Func<T, bool>>(negated, expression.Parameters);
    }
 
    /// <summary>
    /// Combines the first expression with the second using the specified merge function.
    /// </summary>
    static Expression<T> Compose<T>(this Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge)
    {
        // zip parameters (map from parameters of second to parameters of first)
        var map = first.Parameters
            .Select((f, i) => new { f, s = second.Parameters[i] })
            .ToDictionary(p => p.s, p => p.f);
 
        // replace parameters in the second lambda expression with the parameters in the first
        var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body);
 
        // create a merged lambda expression with parameters from the first expression
        return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters);
    }
 
    class ParameterRebinder : ExpressionVisitor
    {
        readonly Dictionary<ParameterExpression, ParameterExpression> map;
 
        ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
        {
            this.map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
        }
 
        public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
        {
            return new ParameterRebinder(map).Visit(exp);
        }
 
        protected override Expression VisitParameter(ParameterExpression p)
        {
            ParameterExpression replacement;
 
            if (map.TryGetValue(p, out replacement))
            {
                p = replacement;
            }
 
            return base.VisitParameter(p);
        }
    }
}
Tags:
Hubs:
+14
Comments 82
Comments Comments 82

Articles