C# Extension для Queryable.Where
От: Alexandr Sulimov Украина www.ase.com.ua
Дата: 21.06.14 05:47
Оценка:
Пишу Extension для IQueryablelist (EntityFramework)

IQueryablelist= db.Users;//Users table
//Поиск  символа "a" в полях которые передаю, это FirstName or LastName
list.ApplyFilterPatern("a", x => x.FirstName, x => x.LastName,...);
//результат работы аналогичен коду
list.Where(x => x.FirstName.ToLower().Contains("a") || x.LastName.ToLower().Contains("a"))


Вот что получилось написать
https://dotnetfiddle.net/zrGPfb

Дальше я запутался с lambda и generics

public class User
    {
        public string FirstName
        {
            get;
            set;
        }

        public string LastName
        {
            get;
            set;
        }

        public string MiddleName
        {
            get;
            set;
        }
    }

    public class Program
    {
        public static void Main()
        {
            List<User> users = new List<User>();
            users.Add(new User { FirstName = "a", LastName = "b" } );
            users.Add(new User { FirstName = "b", LastName = "a" } );
            users.Add(new User { FirstName = "b", LastName = "b" } );
            
            IQueryable<User> l = users.AsQueryable<User>();
            var r = ApplyFilterPatern(l, "a", x => x.FirstName, x => x.LastName);
        }

        public static IQueryable<T> ApplyFilterPatern<T>(IQueryable<T> source, string searchString, params Expression<Func<T, object>>[] properties)
        {
            Expression result = null;
            foreach (var item in properties)
            {
                var lambda = item as LambdaExpression;
                MemberExpression memberExpression;
                if (lambda.Body is UnaryExpression)
                {
                    var unaryExpression = (UnaryExpression)lambda.Body;
                    memberExpression = (MemberExpression)unaryExpression.Operand;
                }
                else
                {
                    memberExpression = (MemberExpression)lambda.Body;
                }

                var expr = Expression.Call(memberExpression, "ToLower", null, null);
                expr = Expression.Call(expr, typeof (string).GetMethod("Contains", new[] { typeof (string)}), Expression.Constant(searchString));
                if (result == null)
                    result = expr;
                else
                    result = Expression.Or(result, expr);
            }

            var parameter = Expression.Parameter(typeof (T), "x");
            var r = Expression.Lambda<Func<T, bool>>(result, parameter);
            Type predType = typeof (Func<, >).MakeGenericType(typeof (T), typeof (bool));
            Type predType2 = typeof (Expression<>).MakeGenericType(predType);
            
            MethodInfo mWhere = typeof (Queryable).GetMethods().Single(
                method => method.Name == "Where" 
                && method.IsGenericMethodDefinition 
                && method.GetGenericArguments().Length == 1 
                && method.GetParameters().Length == 2 
                && method.GetParameters()[1].ParameterType.GetGenericArguments().Length == 1 
                && method.GetParameters()[1].ParameterType.GetGenericArguments()[0].GetGenericArguments().Length == 2);
            
            mWhere = mWhere.MakeGenericMethod(predType2);
            mWhere.Invoke(null, new object[] { source, r } );
            
            return source;
        }
    }


Run-time exception (line 82): Object of type 'System.Linq.EnumerableQuery`1[ASE.User]' cannot be converted to type 'System.Linq.IQueryable`1[System.Linq.Expressions.Expression`1[System.Func`2[ASE.User,System.Boolean]]]'.
Смелости хватает только под ником писать?
Re: C# Extension для Queryable.Where
От: scale_tone Норвегия https://scale-tone.github.io/
Дата: 21.06.14 12:19
Оценка: 15 (1)
Здравствуйте, Alexandr Sulimov, Вы писали:

AS>Run-time exception (line 82): Object of type 'System.Linq.EnumerableQuery`1[ASE.User]' cannot be converted to type 'System.Linq.IQueryable`1[System.Linq.Expressions.Expression`1[System.Func`2[ASE.User,System.Boolean]]]'.



1. Метод Where() резолвится не тот, что нужно.
2. Собирать итоговую лямбду из массива входящих лямбд и вновь созданного Expression.Parameter(typeof(T), "x") не прокатит, т.к. во входящих лямбдах свои собственные параметры, пусть даже и с тем же именем "x". В DebugView выглядит одинаково, но не работает.
3. Возвращать надо не source, а результат mWhere.Invoke().
4. Не Expression.Or, а Expression.OrElse.

В общем, поверхностный рефакторинг Вашего кода привел к этому:
        public static IQueryable<T> ApplyFilterPatern<T>(IQueryable<T> source, string searchString, params Expression<Func<T, object>>[] properties)
        {
            var parameter = Expression.Parameter(typeof(T), "x");

            Expression result = null;
            foreach (var item in properties)
            {
                var lambda = item as LambdaExpression;
                MemberExpression memberExpression;
                if (lambda.Body is UnaryExpression)
                {
                    var unaryExpression = (UnaryExpression)lambda.Body;
                    memberExpression = (MemberExpression)unaryExpression.Operand;
                }
                else
                {
                    memberExpression = (MemberExpression)lambda.Body;
                }

                var propCallExpression = Expression.Property(parameter, (PropertyInfo)memberExpression.Member);

                var expr = Expression.Call(propCallExpression, "ToLower", null, null);
                expr = Expression.Call(expr, typeof (string).GetMethod("Contains", new[] { typeof (string)}), Expression.Constant(searchString));
                if (result == null)
                    result = expr;
                else
                    result = Expression.OrElse(result, expr);
            }

            MethodInfo mWhere =
            (
                from mi in typeof(Queryable).GetMethods(BindingFlags.Public | BindingFlags.Static)
                where mi.Name == "Where"
                let paramInfos = mi.GetParameters()
                where paramInfos.Length == 2
                where (paramInfos[0].ParameterType.GetGenericTypeDefinition() == typeof(IQueryable<>))
                let expressionGenericArgs = paramInfos[1].ParameterType.GetGenericArguments()
                where expressionGenericArgs.Length == 1
                where expressionGenericArgs[0].GetGenericTypeDefinition() == typeof(Func<,>)
                select mi
            )
            .Single();

            mWhere = mWhere.MakeGenericMethod(typeof(T));

            var r = Expression.Lambda<Func<T, bool>>(result, parameter);

            return (IQueryable<T>)mWhere.Invoke(null, new object[] { source, r });
        }


Так даже вроде работает, но код все равно ужасен. Неужели сами не видите?
В целом, на мой взгляд, Вы явно выбрали не тот инструмент для решения задачи.
Expression-ы — это, конечно, звучит круто, но реальная необходимость их применения возникает исключительно редко.
И только в этих очень редких случаях стоит мириться с появлением в коде вышеприведенного месива.
Re[2]: C# Extension для Queryable.Where
От: Alexandr Sulimov Украина www.ase.com.ua
Дата: 22.06.14 09:17
Оценка:
Здравствуйте, scale_tone, Вы писали:

_>1. Метод Where() резолвится не тот, что нужно.

_>2. Собирать итоговую лямбду из массива входящих лямбд и вновь созданного Expression.Parameter(typeof(T), "x") не прокатит, т.к. во входящих лямбдах свои собственные параметры, пусть даже и с тем же именем "x". В DebugView выглядит одинаково, но не работает.
_>3. Возвращать надо не source, а результат mWhere.Invoke().
_>4. Не Expression.Or, а Expression.OrElse.

Огромная боагодарность, несколько дней игрался.

_>Так даже вроде работает, но код все равно ужасен. Неужели сами не видите?

_>В целом, на мой взгляд, Вы явно выбрали не тот инструмент для решения задачи.
_>Expression-ы — это, конечно, звучит круто, но реальная необходимость их применения возникает исключительно редко.
_>И только в этих очень редких случаях стоит мириться с появлением в коде вышеприведенного месива.

А какой инструмент в этом случаее оптимальный?
Для динамических фильтров (сортирвок) я как раз и вышел на Expression.
Смелости хватает только под ником писать?
Re[3]: C# Extension для Queryable.Where
От: scale_tone Норвегия https://scale-tone.github.io/
Дата: 22.06.14 10:46
Оценка: 40 (3) +2
Здравствуйте, Alexandr Sulimov, Вы писали:

AS>А какой инструмент в этом случаее оптимальный?

AS>Для динамических фильтров (сортирвок) я как раз и вышел на Expression.

Можно, например, снабдить саму сущность методом, возвращающим выражение для поиска по ее текстовым полям:

public class User
{
    public int UserId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string MiddleName { get; set; }

    public static Expression<Func<User, bool>> GetFilterExpression(string filter)
    {
        return u =>
            u.FirstName.ToLower().Contains(filter)
            ||
            u.LastName.ToLower().Contains(filter)
        ;
    }
}


Так, по крайней мере, код понятен. И каждой сущности можно приделать по своему такому методу.

И тогда:
foreach (var u in db.Users.Where(Models.User.GetFilterExpression("a")))
{
    Debug.WriteLine(u.FirstName + " " + u.LastName);
}
Re[4]: C# Extension для Queryable.Where
От: Alexandr Sulimov Украина www.ase.com.ua
Дата: 22.06.14 18:31
Оценка:
Здравствуйте, scale_tone, Вы писали:

_>Здравствуйте, Alexandr Sulimov, Вы писали:


AS>>А какой инструмент в этом случаее оптимальный?

AS>>Для динамических фильтров (сортирвок) я как раз и вышел на Expression.

_>Можно, например, снабдить саму сущность методом, возвращающим выражение для поиска по ее текстовым полям:


Будет ли тогда это корректно транслироваться в sql (Entity framework)?

_>
_>foreach (var u in db.Users.Where(Models.User.GetFilterExpression("a")))
_>{
_>    Debug.WriteLine(u.FirstName + " " + u.LastName);
_>}
_>


Нужно обеспечить именно динамический набор полей для фильтрации через единый метод (расширение)
Смелости хватает только под ником писать?
Re[5]: C# Extension для Queryable.Where
От: scale_tone Норвегия https://scale-tone.github.io/
Дата: 22.06.14 19:04
Оценка:
Здравствуйте, Alexandr Sulimov, Вы писали:

AS>Будет ли тогда это корректно транслироваться в sql (Entity framework)?


Ну так работает же. На EF-контексте и проверял.

AS>Нужно обеспечить именно динамический набор полей для фильтрации через единый метод (расширение)


Так вот что значит "динамически"? В Вашем-то варианте оно тоже ни разу не динамическое: набор полей, по которым искать, все равно получается захардкоженным.

А если у Вас задача дать пользователю возможность выбрать из набора полей в некоем окне поиска — то тут гораздо проще прямо SQL-запрос сгенерить. Или сделать хранимку. Чем генерить Expression, по которому потом EF сгенерит SQL-запрос...
Re[6]: C# Extension для Queryable.Where
От: fddima  
Дата: 22.06.14 19:36
Оценка: 41 (3)
Здравствуйте, scale_tone, Вы писали:

_>А если у Вас задача дать пользователю возможность выбрать из набора полей в некоем окне поиска — то тут гораздо проще прямо SQL-запрос сгенерить. Или сделать хранимку. Чем генерить Expression, по которому потом EF сгенерит SQL-запрос...

Не всегда проще...

Упростить создание предиката можно с помощью чего-нибудь такого Dynamically Composing Expression Predicates.
Re: C# Extension для Queryable.Where
От: Аноним  
Дата: 23.06.14 11:45
Оценка:
Здравствуйте, Alexandr Sulimov, Вы писали:

здесь

Вы пытаетесь вызвать метод, а его нужно также сформировать в виде выражения.
Re[7]: C# Extension для Queryable.Where
От: fddima  
Дата: 27.06.14 01:34
Оценка:
Здравствуйте, fddima, Вы писали:

F> Упростить создание предиката можно с помощью чего-нибудь такого Dynamically Composing Expression Predicates.

Ребята — мне страшно стыдно, что за такую известную ссылку — я получил столько оценок.

Давайте теперь я напишу то что я думаю об этом сам. Действительно что думаю об этом сам.

Альбахари этого нужно на вилы — в первую очередь. Пишет дядька конечно умные вещи — но в моём случае, когда я нашел эту ссылку — это было уже игрушкой.

А вообще что я хотел и что я сделал: я хотел что бы было в приложении строка поиска "аля гугл". И в целом — даже получалось. При этом — вывод — абсолютно табличный, при чём конкретных (а не произвольных) сущностей.
В итоге получился in-house фреймворк который способен схавать схему запросов (суть в том что, запросы могут не просто понимать какие-то данные навроде поля=значение, а и просто какие-то "значения" которые позже могут быть во что-то смаплены). Иначе говоря появился довольно специфичный поисковый фреймворк. И да — генерил он эти предикаты далеко нечерез альбахаревскую нечисть (а смысл?!). Даже были идеи сделать его опенсорсным.
Позже пришло понимание и интеграция с Full-Text Search. Так вот — FTS работал из рук вон плохо. Предикат на строку он умудрялся из NVARCHAR конвертировать к VARCHAR и сравнивать его для каждого вхождения что давало ну 90% времени. Ну поверьте уже на слово — если строк хотя бы миллион — с таким говном даже не захочется связываться.
Проблема по факту даже не в этих мелочах, а в том что такие поисхки (которые хотелось) — сиквел в принципе не способен нормально хэндлить. (Т.е. он то нормально всё делает, но только не то, что хотелось бы — ну а по факту — что должен то и делает). Поэтому — вопрос возрос в куда более широкий, а потом вовсе это дело загнило — т.к. аля невостребовано (хотя оно востребовано). Просто потом проект совсем прикрылся. Но опыт — получен. А до того — с сиквелом как раз работали колоссально, скорее что я рассказал — это игрушки то как раз и были.

А. Ну так я что хотел сказать. Склеивание предикатов — путь очень скользкий. =) Каждый его делает видимо по своей нужде. Мне альбахари не подходил. Я предпочёл иметь схему моделей на основе которых могли делать запросы (не забываем, что запросы на самом деле с row-level security). И весь этот велосипед — работал прекрасно. Начиная от парсинья запросов — заканчивая их исполнением.

Но я этими своими крутыми разговорами хотел вот от чего предостереч — не делайте то, что вам не надо. А это такая область когда легко можно создать ещё одну библиотеку. Только когда я этим занималися — их и не было. А теперь — даже альбахари предлагает бредочек свой (бредочек — потому что это пред в практике — но очень удобный, и ещё это бредочек — потому что он работает именно только в этих рамках — а шаг-влево-вправо — выхватите). Не верите? Вперед!
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.