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

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

    Подробнее
    Реклама
    Комментарии 82
    • 0
      А могли бы вы привести примеры «бизнес-процессов», а то само решение понятно, а вот почему именно так — нет.
      • 0
        Синтетический пример
        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
          Ваш ситетический — почти один в один мой реальный :)
          • 0
            Да, он много у кого есть такой. Вы совсем не одиноки.
          • +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);
                }
            }
            
            • 0
              У нас тоже такое есть.

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

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

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


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

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

                  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>>, не более. Вопросы организации бизнес-логики я затрагиваю в других постах, крайний — вот этот.
              • 0
                Возможно Вам будет интересен такой подход к фильтрации данных.
          • –1
            Может проще использовать SQL-запрос?
            • +1
              Как это решит задачу реиспользуемости бизнес-правил фильтрации? Будете SQL-строки собирать?
              • +1
                Вам никто не мешает написать Visitor, который сконвертирует это в запрос к любому хранилищу — у меня в LDAP запросы так генерируются. Правда ушли от дефолтных Expression, что бы ограничить возможные варианты запросов типа MethodCallExpression и тд.

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

                    Ноги отсюда растут — SCIM parser Это была отправная точка ;)
                  • +1
                    что бы ограничить возможные варианты запросов типа MethodCallExpression

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

                    • –1

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

                      • 0

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


                        аналог IQueryable + IQueryProvider

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

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

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

                          У нас специфичное решение и, в общем случае, я бы нарисовал обычных выражений :)
                • +1
                  https://github.com/scottksmith95/LINQKit? (Создал библиотеку автор LINQPad)
                  С ней можно писать
                  .Where(p => NiceRating.Invoke(p.Categoory)
                  и ещё много полезных дополнений.
                  • +2
                    Когда я глядел на LinqKit последний раз, у него были проблемы с .Include(), поэтому я использовал PredicateBuilder от Pete Montgomery.
                  • +1
                     var niceProductsCompilationError = db.Query<Product>.Where(Category.NiceRating); // так нельзя!
                    


                    Либо утро понедельника на меня так влияет, либо я действительно чего-то не понимаю. В чем собственно проблема?
                    • +1
                      Дык, параметр дженерика не тот. Нельзя в Where для Product закидывать дженерик параметрезированный Category.
                      • 0
                        AndreyRyabov правильно ответил. Я дописал, чтобы было понятнее:

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

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


                        q.Where(p => Category.IsNice.Invoke(p.Category).Expand())
                        • +1
                          Есть, в ней еще куча обвесов для EF, которые не нужны, если у вас, например NHibernate. Читаемость этого примера хуже, не находите?
                        • +1

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

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

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


                            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 лет назад :)

                            • 0
                              Не прокатит
                              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)
                              
                              • 0

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


                                Вот прмиер


                                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

                                • 0
                                  Только это не соответствует моей цели. Я хочу получить продукты у которых рейтинг категории > 50. У самого продукта рейтинга нет.
                                  • 0
                                    from c in ctx.Category.NiceRating()
                                    from p in c.Products
                                    where p.Name.StartsWith("a")
                                    select new { p.Id, p.Name };

                                    ?

                                    • +1
                                      Да, так сработает. Видимо, вам важнее написать, чем внимательно прочитать. Я выше написал такой-же пример, но с использованием extension:
                                      dbContext.Categories.Where(Category.NiceRating).SelectMany(x => x.Products);
                                      

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

                                        Мой вариант:


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

                                        Ваш:


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

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


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

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

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


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

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

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


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

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

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


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

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

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

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

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

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

                                                    Через билдер предикатов пример в ссылке, указанной в ответе выше.
                                                    Пример из него:
                                                    public static Expression<Func<Product, bool>> HasNiceRating()
                                                    {
                                                            return prod => prod.Rating > 50;
                                                    }
                                                    

                                                    Это еще сэкономит вызов к QueryProvider при комбинации.
                                                    Тут уж каждому свое :) Я за вариант автора :)
                                                    • 0
                                                      Еще один минус решения через метод расширения — наличие IQueryable. EF далеко не всегда присутствует и QueryProvider может отсутствовать в принципе.
                                                      Те же выражения можно собрать и конвертнуть в фильтр для удаленного ресурса.
                                                      • –1

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

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

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

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

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

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

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

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

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


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

                                                                        • +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 и не завязываясь на конкретного провайдера.
                                                                      • 0
                                                                        В том, что во многих проектах ORM «не работает», потому что тормозит. Razaz уже написал выше, где он видел такие генераторы и в чем их смысл.
                                                  • НЛО прилетело и опубликовало эту надпись здесь
                                                    • 0

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


                                                      dbContext.Products
                                                          .HasGoodCategory()
                                                          .HasTopSeller()

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


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


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

                                                      • НЛО прилетело и опубликовало эту надпись здесь
                                                        • 0

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

                                                          • НЛО прилетело и опубликовало эту надпись здесь
                                                            • 0

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


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

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

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

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

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


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

                                                                    • –1

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

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

                                                                  • НЛО прилетело и опубликовало эту надпись здесь
                                                                    • +1
                                                                      Там моего всего копеечка :) Все есть в C# In Nutshell и автор Compose уже за меня сделал.
                                                                      • 0
                                                                        Добавил в статью. And и Or действительно не сложно написать самому по примеру Compose, но зачем писать, если можно взять готовое?;)
                                              • +1

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


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

                                                • +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
                                                    И соответственно вызов

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

                                                    • НЛО прилетело и опубликовало эту надпись здесь
                                                      • +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
                                                          Проверил на 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();
                                                          
                                                          • НЛО прилетело и опубликовало эту надпись здесь
                                                            • 0
                                                              А чем это удобнее
                                                               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());
                                                                      }
                                                              
                                                              


                                                              То же самое, только кода меньше.
                                                              • 0
                                                                Даже 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/ Кармы не хватает, а так бы плюсик поставил.

                                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.