Допустим, у вас есть товары и категории. В какой-то момент клиент сообщает, что для категорий с рейтингом > 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);
	}


UPD
Razaz добавил еще несколько удобных Extension-методов, позволяющих комбинировать Expression. Вместе с Compose можно создавать цепочки AND и OR. Публикую их в статье as is.
    public static class PredicateBuilder
    {
        public static Expression<Func<T, bool>> True<T>()
        {
            return f => true;
        }

        public static Expression<Func<T, bool>> False<T>()
        {
            return f => false;
        }

        public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1,
            Expression<Func<T, bool>> expr2)
        {
            var invokedExpr = Expression.Invoke(expr2, expr1.Parameters);
            return Expression.Lambda<Func<T, bool>>
                (Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters);
        }

        public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1,
            Expression<Func<T, bool>> expr2)
        {
            var invokedExpr = Expression.Invoke(expr2, expr1.Parameters);
            return Expression.Lambda<Func<T, bool>>
                (Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters);
        }

        public static Expression<Func<TIn, bool>> And<TIn, TInOut>(this Expression<Func<TIn, bool>> target,
            Expression<Func<TIn, TInOut>> input,
            Expression<Func<TInOut, bool>> inOutOut)
        {
            var invoke = Expression.Invoke(input, target.Parameters);
            var invokedExpr = Expression.Invoke(inOutOut, invoke);
            return Expression.Lambda<Func<TIn, bool>>(Expression.AndAlso(target.Body, invokedExpr), target.Parameters);
        }
        public static Expression<Func<TIn, bool>> Or<TIn, TInOut>(this Expression<Func<TIn, bool>> target,
           Expression<Func<TIn, TInOut>> input,
           Expression<Func<TInOut, bool>> inOutOut)
        {

            var invoke = Expression.Invoke(input, target.Parameters);
            var invokedExpr = Expression.Invoke(inOutOut, invoke);
            return Expression.Lambda<Func<TIn, bool>>(Expression.OrElse(target.Body, invokedExpr), target.Parameters);
        }
    }

Для полноты картины ссылка на LinqKit, в котором все это за нас уже сделано. Однако, LinqKit предполагает использование метода AsExpandable и ориентирован, в первую очередь на Entity Framework. Те, кому функциональность LinqKit, как и мне, кажется избыточной могут ограничиться приведенными в статье методами расширений.
Поделиться с друзьями
-->

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


  1. sentyaev
    23.10.2016 21:43

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


    1. marshinov
      23.10.2016 21:50

      Синтетический пример

      var activeAccount = db.Query<Account>().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 для товаров, добавленных до определенной даты одним способом, а после — другим.


      1. Xandrmoro
        23.10.2016 21:59
        +1

        Ваш ситетический — почти один в один мой реальный :)


        1. marshinov
          23.10.2016 22:10

          Да, он много у кого есть такой. Вы совсем не одиноки.


      1. Bronx
        24.10.2016 09:26
        +5

        Критерий NiceRating — это бизнес-правило, к.м.к ему не место в определении сущности БД, лучше определять его где-то извне. Я, например, выносил повторяющиеся выражения в extension methods, где можно делать что хочешь без особой Expression-магии (не отрицая полезность и изящность оной). Например:

        // filter "soft-deleted" entities
        public static IQuerable<T> Active<T>(this IQueriable<T> entities) 
            where T: AuditedEntity
        {
            return entities.Where(entity => !entity.IsDeleted);
        }
        // filter low-rated products
        public static IQuerable<T> WithMinRating<T>(this IQueriable<T> products, int minRating) 
            where T: Product 
        {
            return products.Where(product => product.Category.Rating >= minRating);  
        }
        // do server-side pagination
        public static IQuerable<T> Paginate<T>(this IQueriable<T> items, int total, int skip, int take) 
        {
            // preventing querying DB when there are no items
            if (total == 0) return new List<T>().AsQueriable();
            // using lambdas in Skip/Take to make the SQL query parameterized and its plan reusable.
            return items.Skip(() => skip).Take(() => take);
        }
        ...
        


        Используем:
        public PagedList<Product> Handle(QueryProducts request)
        {
            using (var db = new ProductDb(_connection) 
            {
                var all = db.Products.AsNoTracking().Active().WithMinRating(request.MinRating);
                var total = all.Count();
                val result = all
                    .OrderBy(item => item.Name)
                    .Paginate(total, request.Skip, request.Take)
                    .ToList();
                return new PagedList<Product>(result, total);
            }
        }
        


        1. marshinov
          24.10.2016 10:32

          У нас тоже такое есть.

          У вас пример shared-правил. Их логично выносить. Правила, которые относятся только к сущности я группирую в сущности и считаю, что там самое логичное место, потому что без этой сущности нет и правила.

          Кроме этого мы не используем анемичные модели на стороне ORM. Если нужна легковесная модель, то делаем проекцию в DTO.


          1. SergeyEgorov
            24.10.2016 12:23
            +1

            Похоже я чего-то недопонял, так что если не возражаете, тоже задам вопрос на тему места размещения экземпляров деревьев выражений.


            А если завтра появится требование применять отдельные бизнес-правила к категориям, с рейтингом ниже 20, назовем их к примеру PoorRating? Что будем делать?


            1. marshinov
              24.10.2016 12:37

              Сразу оговорюсь, что этот пример подходит для каких-то приложений и не подходит для других. Зависит от сложности приложения и договоренностей внутри команды.

              public class Category : HasIdBase<int>
              {
                  public static readonly Expression<Func<Category, bool>> NiceRating = x => x.Rating > 50;
              
                  public static readonly Expression<Func<Category, bool>> PoorRating= x => x.Rating < 50;
              
                  public static readonly Expression<Func<Category, bool>> NiceName = x =>     
                       x.Name.StartsWith("Ктулху");
              
                  //...
                }
              

              Где именно находится условие: в классе сущности или отдельном месте — вопрос, который решает команда. Например, можно вынести все в спецификации (мы поступаем часто именно так).

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


  1. xRay
    23.10.2016 23:14
    -1

    Может проще использовать SQL-запрос?


    1. marshinov
      23.10.2016 23:33
      +1

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


    1. Razaz
      24.10.2016 00:46
      +1

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


      1. marshinov
        24.10.2016 00:49

        А чем заменили Expression? Свой API?


        1. Razaz
          24.10.2016 01:02
          +1

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

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


      1. chumakov-ilya
        25.10.2016 13:21
        +1

        что бы ограничить возможные варианты запросов типа MethodCallExpression

        А зачем? Можно пример? Неужели отбиваются трудозатраты на реализацию IQueryProvider?


        1. mayorovp
          25.10.2016 14:00
          -1

          Во-первых, реализация IQueryProvider — не самый трудный этап. Во-вторых, если они ушли от Expression — значит, они не стали реализовывать IQueryProvider!


          1. chumakov-ilya
            25.10.2016 16:33

            читаем внимательно


            аналог IQueryable + IQueryProvider

            если вам так принципиально, можете читать мой предыдущий комментарий как трудозатраты на реализацию аналога IQueryProvider


            1. Razaz
              25.10.2016 17:38
              +2

              Трудозатраты — 2 рабочих дня + вечер ковыряния из дома :P Результат — специфичный для доменной области механизм построения запросов, который генерит заметно меньше объектов и жестко ограничивает возможные варианты композиции, что делает написание разных потребителей заметно проще и большую часть ошибок можно отсечь на этапе компиляции.
              Например IPermissionQuery имеет свой набор доступных выражений.
              Плюс упростилось само дерево выражений и транслировать его стало проще :)

              В принципе написать IQueryProvider не так сложно, если ковыряться в этой теме, но у нас есть композиция сторов и это автоматом делает IQueryProvider не очень удобным, так как хочется собрать запрос и пихнуть в 10 сторов, которые могут быть удаленными, локальными, реляционными и нет, включая файловую систему, а рисовать композицию внутри него не шибко хочется. Плюс логика материализации зависит от стора.

              У нас специфичное решение и, в общем случае, я бы нарисовал обычных выражений :)


  1. KIVan
    24.10.2016 04:09
    +1

    https://github.com/scottksmith95/LINQKit? (Создал библиотеку автор LINQPad)
    С ней можно писать
    .Where(p => NiceRating.Invoke(p.Categoory)
    и ещё много полезных дополнений.


    1. Bronx
      25.10.2016 20:30
      +2

      Когда я глядел на LinqKit последний раз, у него были проблемы с .Include(), поэтому я использовал PredicateBuilder от Pete Montgomery.


  1. Hydro
    24.10.2016 07:20
    +1

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


    Либо утро понедельника на меня так влияет, либо я действительно чего-то не понимаю. В чем собственно проблема?


    1. AndreyRubankov
      24.10.2016 08:27
      +1

      Дык, параметр дженерика не тот. Нельзя в Where для Product закидывать дженерик параметрезированный Category.


    1. marshinov
      24.10.2016 10:48

      AndreyRyabov правильно ответил. Я дописал, чтобы было понятнее:

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


  1. Gebb
    24.10.2016 10:18
    +1

    Есть ещё библиотека LinqKit, тоже позволяющая комбинировать выражения. Было бы что-то вроде


    q.Where(p => Category.IsNice.Invoke(p.Category).Expand())


    1. marshinov
      24.10.2016 10:27
      +1

      Есть, в ней еще куча обвесов для EF, которые не нужны, если у вас, например NHibernate. Читаемость этого примера хуже, не находите?


  1. SergeyEgorov
    24.10.2016 10:22
    +1

    Если я правильно помню, сигнатура Expression.Parameterподразумевает следующий набор аргументов (Type type, string name). Соответственно приведенный вами код, компилироваться не должен, из-за невозможности преобразования TIn в string в строке 9. Или я чего-то неправильно прочитал?


    1. marshinov
      24.10.2016 10:23

      Спасибо, я немного ошибся, когда код вставлял. Поправил.


  1. gandjustas
    24.10.2016 11:05

    Надо использовать интерфейсы для таких вещей


    public interface IRatingable
    {
        int Rating { get; set; }
    }
    
    public static class IRatingableExtensions
    {
        public static IQueryable<T> NiceRating<T>(this IQueryable<T> q) 
            where T : IRatingable, class
        {
            return q.Where(x => x.Rating > 50);
        }
    }

    Я на эту тему пост писал 6 лет назад :)


    1. marshinov
      24.10.2016 11:09

      Не прокатит

      var products = db.Query<Product>().Where(x => x.Category.NiceRating()); // не пойдет
      var products = db.Query<Product>().NiceRating(); // не пойдет
      

      Вот так можно обойти, но я не уверен, как разные провайдеры такое реализуют (не тестировал). И такой вариант не подойдет, если нужно обрабатывать 2 связанных класса.
      dbContext.Categories.Where(Category.NiceRating).SelectMany(x => x.Products)
      


      1. gandjustas
        24.10.2016 11:31

        Куда не пойдет?


        Вот прмиер


        public interface IWithRating
        {
            int Rating { get; set; }
        }
        public class Product:IWithRating
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public int Rating { get; set; }
        }
        
        public class StoreContext : DbContext
        {
            public DbSet<Product> Products { get; set; }
        }
        
        public static class RatingExtensions
        {
            public static IQueryable<T> NiceRating<T>(this IQueryable<T> q) where T : class, IWithRating
            {
                return q.Where(x => x.Rating > 50);
            }
        }
        
        class Program
        {
        
            static void Main(string[] args)
            {
                using (var ctx = new StoreContext())
                {
                    ctx.Database.Log = Console.WriteLine;
        
                    var q = from p in ctx.Products.NiceRating()
                            where p.Name.StartsWith("a")
                            select new { p.Id, p.Name };
                    q.ToArray();
                }
            }
        }

        В консоли внезапно:


        SELECT
            [Extent1].[Id] AS [Id],
            [Extent1].[Name] AS [Name]
            FROM [dbo].[Products] AS [Extent1]
            WHERE ([Extent1].[Rating] > 50) AND ([Extent1].[Name] LIKE N'a%')

        Вот ссылка на Gist https://gist.github.com/gandjustas/e65c8602b59c86966616fa29a69fe9a6


        1. marshinov
          24.10.2016 11:35

          Только это не соответствует моей цели. Я хочу получить продукты у которых рейтинг категории > 50. У самого продукта рейтинга нет.


          1. gandjustas
            24.10.2016 11:41

            from c in ctx.Category.NiceRating()
            from p in c.Products
            where p.Name.StartsWith("a")
            select new { p.Id, p.Name };

            ?


            1. marshinov
              24.10.2016 12:00
              +1

              Да, так сработает. Видимо, вам важнее написать, чем внимательно прочитать. Я выше написал такой-же пример, но с использованием extension:

              dbContext.Categories.Where(Category.NiceRating).SelectMany(x => x.Products);
              

              Это один из способов обойти ограничение. Я привел другой. Мне обычно удобнее делать запрос к целевой сущности. Давайте закончим это обсуждение.


              1. gandjustas
                24.10.2016 12:06

                Мой вариант:


                dbContext.Categories.NiceRating().SelectMany(x => x.Products);

                Ваш:


                dbContext.Products.Where(x => x.Category, Category.NiceRating).

                Ваш вариант длиннее, discoverability хуже, какие-то странные манипуляции с expression делает.


                Мой вариант гораздо гибче, так как в интерфейсе может быть несколько полей, метод-расширение может работать с несколькими интерфейсами.


                1. marshinov
                  24.10.2016 12:15
                  +1

                  Это не странные манипуляции.Попробуйте записать лябмду x => x.Category.Rating > 50 и посмотрите какой получится Expression Tree. Это полезно для понимания, что «под капотом» у LINQ. И это не ортогональные вещи. Можно комбинировать интерфейсы и экстеншны, там где нужно, а где не нужно — не использовать.
                  Я же не агитирую. Вам не нравится — вы не будете использовать, ну и не используйте.


                  1. gandjustas
                    24.10.2016 13:57

                    Я привел решение конкретной задачи, которое не уступает вашему. Хачить деревья выражений для такой задачи вовсе необязательно.


                    Про хаки с деревьями выражений я также писал 6 лет назад http://blog.gandjustas.ru/2010/06/13/expression-tree/


                    1. Razaz
                      24.10.2016 15:37
                      +1

                      Ваш вариант менее информативен. Что за метод NiceRating и что он делает непонятно. И непонятно почему из него можно сделать SelectMany, а не Select.
                      Второй вариант уже понятнее с первого взгялда.
                      Ваш вариант был бы понятнее в виде: dbContext.Categories.WithNiceRating();
                      Но это сугубо ИМХО.
                      Ничего страшного в деревьях выражений нет — очень мощный и гибкий механизм.


                      1. gandjustas
                        24.10.2016 15:44

                        Докопаться и до столба можно. Мы тут не названия методов обсуждаем, а подход к декомпозиции Linq запросов.


                        Можно устраивать игрища к с деревьями выражений, но в большинстве случаев достаточно набора методов-расширений.


                        1. Razaz
                          24.10.2016 16:22

                          1. Декомпозиция должна быть читаемой и выразительной. Ваш вариант выглядит странно хотя бы уже за SelectMany(x => x.Products) — это не интуитивно понятно.
                          2. PredicateBuilder можно нагуглить на пару секунд или прочитав C# in Nutshell.
                          Что-то отличное от этого имеет смысл делать если есть специфические требования, но в обычных бизнес приложениях это редкость.


                      1. chumakov-ilya
                        24.10.2016 19:28
                        +2

                        И непонятно почему из него можно сделать SelectMany, а не Select.

                        Непонятно будет только тем, кто не понимает SelectMany, как вы считаете? С моей точки зрения, оба варианта читаются одинаково, но вариант с extention method проще технологически. Впрочем, было бы интереснее узнать, генерит ли EF эквивалентный SQL в обоих случаях.


                        Ничего страшного в деревьях выражений нет — очень мощный и гибкий механизм.

                        К сожалению, со своими ограничениями (часть из списка уже пофиксили) и относительно медленным компилятором (люди пишут свои).


                        1. Razaz
                          24.10.2016 20:42
                          +1

                          Странный аргумент. Для меня SelectMany по категориям выглядит чужеродно.
                          Логично делать выборку по множеству продуктов, с фильтром по категориям, а не наоборот.

                          Ограничения есть всегда. Тут вопрос в том, критичны ли они. Я выше уже отписался, что отказался от встроенных выражений по определенным причинам.
                          Скомпилированные выражения можно кешировать. Но тут все зависит от вашего кейса. В случае с этим вопросом мне кажется будет эффективнее просто сделать And через PredicateBuilder.

                          А если уж опираться в производительность, то LINQ на hot paths лучше избегать вообще :)

                          Вот что будет внутри Where — Queryable.Where

                          Через билдер предикатов пример в ссылке, указанной в ответе выше.
                          Пример из него:

                          public static Expression<Func<Product, bool>> HasNiceRating()
                          {
                                  return prod => prod.Rating > 50;
                          }
                          

                          Это еще сэкономит вызов к QueryProvider при комбинации.
                          Тут уж каждому свое :) Я за вариант автора :)


                        1. Razaz
                          24.10.2016 21:22

                          Еще один минус решения через метод расширения — наличие IQueryable. EF далеко не всегда присутствует и QueryProvider может отсутствовать в принципе.
                          Те же выражения можно собрать и конвертнуть в фильтр для удаленного ресурса.


                          1. gandjustas
                            25.10.2016 00:53
                            -1

                            Если нет IQueryable, то о чем разговор вообще? Как без него декомпозировать запросы?


                            1. Razaz
                              25.10.2016 02:08

                              Просто оставлю это здесь — Практическое руководство. Реализация обхода дерева выражения И еще тут: The Query Translator
                              Для трансляции дерева выражений IQueryable не нужен.


                              1. gandjustas
                                25.10.2016 02:55

                                Ты предлагаешь IQueryable самому реализовать? Это минимум два человеко-года по оценке Microsoft.


                                1. Razaz
                                  25.10.2016 02:59

                                  Просто Visitor не катит? Это пара человеко дней. Почитайте внимательно для начала. Там дан пример простейшего генератора SQL запросов через обход дерева выражений.


                                  1. gandjustas
                                    25.10.2016 03:55

                                    В том и прикол, что "простейшего". Генератор запросов уровня linq2sql\ef — минимум два человеко-года.


                                    1. Razaz
                                      25.10.2016 09:49

                                      Все зависит от задачи. Простейший генератор удовлетворяет в большинстве случаев. Все доп кейсы по необходимости легко реализуются. Но, как выше уже не раз писал, а вы игнорировали — выражения используются и без QueryProvider.


                                      1. gandjustas
                                        25.10.2016 11:16

                                        Не видел в живой природе использования таких генераторов? И в чем смысл когда есть orm?


                                        1. Razaz
                                          25.10.2016 11:29

                                          Конвертация в LDAP, конвертация в query string, единый набор выражений при композиции хранилищь с разным способом доступа(даже если и есть QueryProvider — они разные), парсинг выражений и их эвалюейшен относительно объекта(включая rewrite например).
                                          EF, как ОРМ, слишком жирная абстракция. Для проектов с низкими требованиями к слою хранения данных вполне подходит. Как только вы выходите за рамки SqlServer — он превращается в тыкву на костылях.


                                          1. gandjustas
                                            25.10.2016 12:09

                                            Даже для Odata (конвертация в querystring) написан очень даже провайдер. Linq2LDAP (https://linqtoldap.codeplex.com/) тоже не самая простая штука.


                                            Я не понимаю о каких "простых" случаях идет речь.


                                            1. Razaz
                                              25.10.2016 13:49
                                              +1

                                              Если нет OData, что делать? Если у вас другой протокол? Проблема в том что надо независящий от провайдера механизм. Самому написать Visitor не проблема.

                                              А вы попробуйте Linq2LDAP для начала, а потом приводите это чудо в пример. Я вот использовал его в проде — выкинул нафиг и написал свой транслятор запросов, так как количество аллокейшенов там просто зашкаливало.

                                              Вот такой запрос чем собрать? Это в query string:
                                              "((userName lk \"*Jacob*\") and (title gt \«Intern\» or title eq \«Employee\») or lastModified ge \«2011-05-13T04:42:34Z\»)"

                                              Все ваши рассуждения строятся на том что всегда есть добрый дядя, который напишет провайдер. Провайдер это ничто иное как создание IQueryable + набор трансляторов. Сделать набор трансляторов можно самому под конкретные требования. Создать точку входа можно через PredicateBuilder и им же комбинировать, не плодя новый IQueryable и не завязываясь на конкретного провайдера.


                                        1. marshinov
                                          25.10.2016 11:36

                                          В том, что во многих проектах ORM «не работает», потому что тормозит. Razaz уже написал выше, где он видел такие генераторы и в чем их смысл.


                                          1. gandjustas
                                            25.10.2016 12:57

                                            Что именно тормозит?


                    1. ggrnd0
                      24.10.2016 22:55

                      И все же ваш вариант уступает в гибкости.
                      Что вы будете делать если у поиска продукта будет у фильтр по 2 полям-ссылкам?


                      class Product{
                          Category;
                          Seller;
                      }
                      class Category{
                          List<Product> Products;
                          Expression<Func<Category, bool>> NiceRating = c => c.Rating >= 50;
                      }
                      class Seller{
                          List<Product> Products;
                          Expression<Func<Seller, bool>> GoodSeller = s => s.Rating >= 4;
                      }

                      Следуя приведенный автором метод расширения можно написать так:


                      dbContext.Products
                          .Where(x => x.Category, Category.NiceRating)
                          .Where(x => x.Seller, Seller.GoodSeller)


                      1. gandjustas
                        25.10.2016 01:06

                        И тогда бизнес-логика прекрасно выглядит:


                        dbContext.Products
                            .HasGoodCategory()
                            .HasTopSeller()

                        Те же предикаты уезжают внутрь экстеншенов.


                        При этом остается возможность в экстеншенах дополнительную логику написать, вычисления какие-нибудь например. С деревьями так не получится


                        Но гораздо интереснее становится, когда у многих сущностей появляются одинаковые свойства. Например флажки IsActive\IsDeleted, разные рейтинги, даже Id и Title поля, которые и так почти всегда есть. В этом случае мы не просто декомпозируем запрос, но и повторно используем логику.


                        1. ggrnd0
                          25.10.2016 02:33

                          Отличие


                          dbContext.Products
                              .Where(x => x.Category, Category.NiceRating)
                              .Where(x => x.Seller, Seller.GoodSeller)

                          от


                          dbContext.Products
                              .HasGoodCategory()
                              .HasTopSeller()

                          в переиспользовании Category.NiceRating и Seller.GoodSeller.


                          Их не надо дублировать в виде Product.NiceRating и Product.GoodSeller:


                          class Product{
                              Category;
                              Seller;
                              Expression<Func<Product, bool>> NiceRating = p => p.Category.Rating >= 50;
                              Expression<Func<Product, bool>> GoodSeller = p => p.Seller.Rating >= 4;
                          }
                          class Category{
                              List<Product> Products;
                              Expression<Func<Category, bool>> NiceRating = c => c.Rating >= 50;
                          }
                          class Seller{
                              List<Product> Products;
                              Expression<Func<Seller, bool>> GoodSeller = s => s.Rating >= 4;
                          }

                          сами условия пишутся один раз.
                          Так же можно добавить методы расширения And/Or и состряпать порядочный такой predicate-builder


                          1. gandjustas
                            25.10.2016 02:54

                            Без конкретного сценария непонятно что обсуждаем.
                            Я говорю про одинаковое поведение для разных типов. Ты говоришь о похожем поведении для разных типов. При этом в твоем случае также присутствует дублирование кода.


                            1. ggrnd0
                              25.10.2016 10:17

                              Где дублирование? Третий листинг — пример дублирования от которого избавляет метод расширения автора.
                              Ориентироваться надо на первый листинг


                              1. gandjustas
                                25.10.2016 11:20

                                Это ты о чем сейчас? Ты предлягаешь написать две лямбды, я предлагаю два метода-расширения. И в том, и в другом случае используются похожие поля, но простого способа свести их к одной лямбде\одному методу нет.


                                Меня интересует другой случай. На практике чаще приходится сталкиваться с одинаковым поведением полей в разных сущностях. Тогда интерфейсы и расширения удобнее и гибче.


                                1. marshinov
                                  25.10.2016 11:33

                                  И тогда это другой случай, но чукча — писатель, а не читатель ;)


                                  1. gandjustas
                                    25.10.2016 13:00

                                    Это тот самый случай, описанный в посте автора. А про композицию — уже твои фантазии, которые от реальности ушли.


                                1. Razaz
                                  25.10.2016 11:41
                                  -1

                                  And, Or как будете делать?
                                  Внутри в вашем Where будет похожий код, только он еще сходит в QueryProvider и создаст новый IQueryable.
                                  Вот вам с интерфейсами :)


                                  1. gandjustas
                                    25.10.2016 13:01

                                    And, Or как будете делать?

                                    Какую проблему решаем?


                                    Я and и or делал много раз без predicate builder.
                                    Я что-то не так делал?


                                  1. mayorovp
                                    25.10.2016 14:07
                                    -1

                                    Вы свой код проверяли вообще? У вас в метод Where передается всегда истинное условие...


                                    1. Razaz
                                      25.10.2016 14:19
                                      +1

                                      Извините, ночью как-то не до этого было. Это пример организации и комбинации, а не код для продакшена. Можете поправить и написать.


                                    1. Razaz
                                      25.10.2016 14:38
                                      +1

                                      Чтоб вас так не коробило — поправил.


                                1. ggrnd0
                                  25.10.2016 15:05
                                  +1

                                  Я предлагаю 2 ляьбды вместо 4 методов расширений.
                                  Так как критерии NiceRating и GoodSeller должны быть применимы не только в Продукту, но и к Категории/Продавец соответственно.


                                  Условия предикатов могут быть сколько угодно сложными.
                                  А их использование может не ограничиваться только 2мя сущностями.


                                  Если каждый критерий копипастить для нескольких сущностей, изменения какого-либо критерия будет приводить к его изменению в каждой из этих сущностей.


                                1. ggrnd0
                                  25.10.2016 15:09

                                  Ваш случай не настолько другой.
                                  Если вместо привязки предиката к сущности, привязывать его к интерфейсу — предложенный автором подход так же применим!


                                  Просто делается замена
                                  Category -> IHaveRating
                                  Seller -> IHaveStars


                                  И если продукт или его поля реализуют указанные интерфейсы к ним можно применить указанные предикаты.
                                  Другое дело, что эта задача тривиальна, совсем. Это просто декомпозиция и выделение интерфейса.


                      1. Razaz
                        25.10.2016 02:15
                        +1

                        Этот вариант сразу отпадает если захочется сделать Or. Приехали. Не говоря опять о том, что без реализации полноценного QueryProvider это нерабочий вариант. Прибивать правила бизнес логики к слою данных — не самое мудрое решение.
                        Как вариант накидал предикат билдер с тем, что у автора в статье. В принципе можно красивее сделать, но думаю для базы хватит.
                        Gist


                        1. ggrnd0
                          25.10.2016 02:38
                          +1

                          Ваш predicate-builder — хорошее дополнение к статье.


                          1. Razaz
                            25.10.2016 02:41
                            +1

                            Там моего всего копеечка :) Все есть в C# In Nutshell и автор Compose уже за меня сделал.


                            1. marshinov
                              25.10.2016 10:36

                              Добавил в статью. And и Or действительно не сложно написать самому по примеру Compose, но зачем писать, если можно взять готовое?;)


  1. mayorovp
    24.10.2016 16:32
    +1

    Нельзя не упомянуть еще один инструмент: DelegateDecompiler от alexanderzaytsev — библиотека, которая преобразует IL в Expressions. Пост про нее на Хабре: https://habrahabr.ru/post/155437/


    И мой форк, где решена проблема с Include, которую не видит автор: DelegateDecompiler


  1. Serginio1
    26.10.2016 12:48
    +1

    Спасибо за статью. Решил свой пробел в Expression восполнить. Поэто му по мотивам
    Динамическое построение Linq запроса

    По аналогии

    public static IQueryable<T> Beetwen<T>(this IQueryable<T> src, Expression<Func<T, DateTime>> propertyExpression, DateTime from, DateTime to)
    {
        Expression<Func<DateTime, bool>> func = d => d >= from && d <= to;
    
        return src.Where(Expression.Lambda<Func<T, bool>>(Expression.Invoke(func, propertyExpression.Body), propertyExpression.Parameters.First()));
    } 
    
    

    сделать

    public static IQueryable<T> NiceRating<T>(this IQueryable<T> q,Expression<Func<T, Category>> propertyExpression) 
         {
            Expression<Func<Category, bool>> func = x => x.Rating > 50;
     return src.Where(Expression.Lambda<Func<T, bool>>(Expression.Invoke(func, propertyExpression.Body), propertyExpression.Parameters.First()));
        }
    
    


    Прошу прощения, за некомпетентность. Решил наверстать упущенное


    1. Serginio1
      26.10.2016 16:24
      +1

      И соответственно вызов

      dbContext.Products
          .NiceRating(x => x.Category)
      


      1. ggrnd0
        26.10.2016 16:42
        +1

        Лучше воспользоваться методом Compose


            return src.Where(propertyExpression.Compose(func));

        Но тогда будет не очень удобно использовать с IQueriable


        1. Serginio1
          26.10.2016 16:59
          +1

          Ну тут уж на любителя. Написать один раз
          С таким же успехом можнгно И отдельную Функцию Написать

            public static IQueryable<T> Compose<T,Y>(this IQueryable<T> src,Expression<Func<T, Y>> propertyExpression,Expression<Func<Y, bool>> func )
          {
          return src.Where(Expression.Lambda<Func<T, bool>>(Expression.Invoke(func, propertyExpression.Body), propertyExpression.Parameters.First()));
          }
          


          Сейчас проверю
          Это не принципиально.
          Главное использование

          dbContext.Products
              .NiceRating(x => x.Category)
          


          А можно ссылочку на Compose


          1. Serginio1
            26.10.2016 17:10
            +1

            Проверил на IEnumerable

            public  class TestExpression
                {
                   public DateTime Created { get; set; }
                  
                    public TestExpression(DateTime Created)
                    {
            
                        this.Created = Created;
            
                    }
            
                }
            
                public  static class РасширениеLinq
                    {
            
                    public static IEnumerable<T> Compose<T, Y>(this IEnumerable<T> src, Expression<Func<T, Y>> propertyExpression, Expression<Func<Y, bool>> func)
                    {
                        return src.Where(Expression.Lambda<Func<T, bool>>(Expression.Invoke(func, propertyExpression.Body), propertyExpression.Parameters.First()).Compile());
                    }
            
                    public static IEnumerable<T> Beetwen<T>(this IEnumerable<T> src, System.Linq.Expressions.Expression<Func<T, DateTime>> propertyExpression, DateTime from, DateTime to)
                {
                    System.Linq.Expressions.Expression<Func<DateTime, bool>> func = d => d >= from && d <= to;
                        
                    return src.Where(System.Linq.Expressions.Expression.Lambda<Func<T, bool>>(System.Linq.Expressions.Expression.Invoke(func, propertyExpression.Body), propertyExpression.Parameters.First()).Compile());
                }
            
                    public static IEnumerable<T> Beetwen2<T>(this IEnumerable<T> src, System.Linq.Expressions.Expression<Func<T, DateTime>> propertyExpression, DateTime from, DateTime to)
                    {
                        System.Linq.Expressions.Expression<Func<DateTime, bool>> func = d => d >= from && d <= to;
            
                        return src.Compose(propertyExpression, func);
                    }
            
                }
            


            И использование
              var Дата = DateTime.Now;
                        var d = new List<TestExpression>()
                        {
            
                            new TestExpression(DateTime.Now)
                        };
            
                        var res = d.Beetwen2(_ => _.Created, Дата.AddDays(-1), Дата.AddDays(1)).FirstOrDefault();
                        res = d.Beetwen2(_ => _.Created, Дата.AddDays(1), Дата.AddDays(1)).FirstOrDefault();
            


          1. ggrnd0
            26.10.2016 17:10

            Compose представлен автором в статье. Сразу после


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

            Habr съел тег, приведенный фильтр будет неудобно использовать с IQueryable<Category>.
            А именно


            dbContext.Categories.NiceRating(x => x)

            Вариант автора удобнее.


            1. Serginio1
              26.10.2016 17:15

              А чем это удобнее

               public static IEnumerable<T> Compose<T, Y>(this IEnumerable<T> src, Expression<Func<T, Y>> propertyExpression, Expression<Func<Y, bool>> func)
                      {
                          return src.Where(Expression.Lambda<Func<T, bool>>(Expression.Invoke(func, propertyExpression.Body), propertyExpression.Parameters.First()).Compile());
                      }
              
              


              То же самое, только кода меньше.


              1. Serginio1
                27.10.2016 10:04

                Даже First не нужен

                public static IEnumerable<T> Compose<T, Y>(this IEnumerable<T> src, Expression<Func<T, Y>> propertyExpression, Expression<Func<Y, bool>> func)
                        {
                            return src.Where(Expression.Lambda<Func<T, bool>>(Expression.Invoke(func, propertyExpression.Body), propertyExpression.Parameters).Compile());
                        }
                


                Я к тому, что когда начал разбираться с примером, то решил восполнить свои пробелы в Expression и для меня пример Динамическое построение Linq запроса

                Показался более понятным. А автору большой респект за Expression/ Кармы не хватает, а так бы плюсик поставил.