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

Допустим, у вас есть товары и категории. В какой-то момент клиент сообщает, что для категорий с рейтингом > 50 необходимо использовать другие бизнес-процессы. У вас достаточно опыта и вы понимаете, что где сегодня 50 завтра будет 127.37 и хотите избежать появления магических чисел в коде, поэтому делаете так:
    public class Category : HasIdBase
    {
        public static readonly Expression> NiceRating = x => x.Rating > 50;

       //...
    }

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

К сожалению, этот номер не пройдет, если вы хотите выбрать продукты из соответствующих категорий:
    public class Product: HasIdBase
    {
        public virtual Category Category { get; set; }

       //...
    }

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

К счастью, устранить данный недостаток довольно просто!

     // Фактически мы реализуем композицию выражений,
     // которая даст нам выражение, соответствующее композиции целевых функций
     public static Expression> Compose(
             this Expression> input
            , Expression> inOutOut
            , TIn inParam = default(TIn))
        {
            // это параметр x => blah-blah. Для LINQ нам нужен null
            var param = Expression.Parameter(typeof(TIn), inParam);
            // получаем объект, к которому применяется выражение
            var invoke = Expression.Invoke(input, param);
            // и выполняем "получи объект и примени к нему его выражение"
            var res = Expression.Invoke(inOutOut, invoke);
            
            // возвращаем лямбду нужного типа
            return Expression.Lambda>(res, param);
        }
        
        // Добавляем "продвинутый" вариант Where
        public static IQueryable Where(this IQueryable queryable
            , Expression> prop
            , Expression> 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);
	}

Спасибо за внимание!

Комментарии (9)

  • 23 октября 2016 в 21:43

    0

    А могли бы вы привести примеры «бизнес-процессов», а то само решение понятно, а вот почему именно так — нет.
    • 23 октября 2016 в 21:50

      0

      Синтетический пример
      var activeAccount = db.Query().Where(x => x.IsActive && x.IsNotDeleted && x.Balance > 0 && x.LastVisited > new DateTime(2015, 01, 01) && x.SuperPuper > 100500 && x.Whatever)
      

      Сначала мы считали, что активные аккаунты, это те, что IsActive, потом ввели soft-delte, потом стали учитывать баланс, потом дату последнего посещения и пошло-поехало. Если эти правила не группировать, а копипастить, то рано или поздно где-то забудем поменять. Значит условия нужно группировать.

      Из реальных кейсов бизнес-процессов, однажды клиент попросил формировать URL для товаров, добавленных до определенной даты одним способом, а после — другим.

      • 23 октября 2016 в 21:59

        +1

        Ваш ситетический — почти один в один мой реальный :)
        • 23 октября 2016 в 22:10

          0

          Да, он много у кого есть такой. Вы совсем не одиноки.
  • 23 октября 2016 в 23:14

    0

    Может проще использовать SQL-запрос?
    • 23 октября 2016 в 23:33

      0

      Как это решит задачу реиспользуемости бизнес-правил фильтрации? Будете SQL-строки собирать?
    • 24 октября 2016 в 00:46

      +1

      Вам никто не мешает написать Visitor, который сконвертирует это в запрос к любому хранилищу — у меня в LDAP запросы так генерируются. Правда ушли от дефолтных Expression, что бы ограничить возможные варианты запросов типа MethodCallExpression и тд.
      • 24 октября 2016 в 00:49

        0

        А чем заменили Expression? Свой API?
        • 24 октября 2016 в 01:02 (комментарий был изменён)

          0

          Угу. Простенько и обрезано под бизнес область. Соответственно свой аналог IQueryable + IQueryProvider с минимальным функционалом. Но на LDAP или SQL легко раскладывать. И можно свои подтипы для IQueryable рисовать и ограничивать набор выражений используемых в запросах и добавлять специфику.

          Ноги отсюда растут — SCIM parser Это была отправная точка ;)

© Habrahabr.ru