Динамические Linq-запросы или приручаем деревья выражений

    Введение


    Linq to Entity позволяет очень выразительно со статической проверкой типов писать сложные запросы. Но иногда надо нужно сделать запросы чуть динамичнее. Например, добавить сортировку, когда имя колонки задано строкой.
    Т.е. написать что то вроде:
    var query = from customer in context.Customers
        select customer;
    //ошибка! не компилируется.
    query = query.OrderBy("name");
    var data = query.ToList();
    

    На помощь в этом случае придет динамическое построение деревьев выражений (expression trees). Правда одних выражений будет недостаточно, потребуется динамически находить и конструировать шаблонные методы. Но и это не так сложно. Подробности ниже.


    Сортировка запросов linq to entity, когда имя поля задано строкой


    Вообще для упорядочивания данных, возвращаемых Linq — запросом, применяются 4 метода:
    OrderBy
    OrderByDescending
    ThenBy
    ThenByDescending
    

    Напишем обобщенный метод, который будет принимать 3 аргумента.
    public static IOrderedQueryable<T> ApplyOrder<T>(
                this IQueryable<T> source,
                string property,
                string methodName
                )
    

    Где source — исходный IQueriable, к которому нужно добавить упорядочивание; property – имя свойства, по которому производится сортировка; methodName – название метода упорядочивания из списка выше. Конечно, в боевом коде ApplyOrder сделан приватным, и пользователь имеет дело с методами:
    OrderBy (this IQueryable<T> source, string property)
    OrderByDescending (this IQueryable<T> source, string property)
    ThenBy (this IQueryable<T> source, string property)
    ThenByDescending (this IQueryable<T> source, string property)
    

    Которые устроены тривиально и в итоге вызывают ApplyOrder.
    public static IOrderedQueryable<T> ApplyOrder<T>(
        this IQueryable<T> source,
        string property,
        string methodName
        )
    {
        //Создаем аргумент лямбда выражения. в данном случае мы формируем лямбду x => x.property, а значит аргумент будет один
        var arg = Expression.Parameter(typeof(T), "x");
        //Начинаем построение дерева выражения. x => x. Возвращаем переданный аргумент без изменений
        Expression expr = arg;
    
        //Второй и последний шаг построения дерева выражения. Обращаемся к свойству property. x => x.property
        expr = Expression.Property(expr, property);
    
        //Создаем из дерева выражений лямбду, привязываем к ней аргумент
        var lambda = Expression.Lambda(expr, arg);
    
        //Находим в классе Queryable метод с именем methodName. Магия поиска и создания шаблонного метода чуть ниже
        var method = typeof(Queryable).GetGenericMethod(
            methodName,
            //Типы аргументов шаблона метода
            new[] { typeof(T), expr.Type },
            //Типы параметров метода
            new[] { source.GetType(), lambda.GetType() }
            );
        //Выполняем метод, передавая ему неявный параметр this и лямбду. 
        //Т.е. выполняем source.OrderBy(x => x.property);
        return (IOrderedQueryable<T>)method.Invoke(null, new object[] { source, lambda });
    }
    

    Комментарии поясняют что происходит. Сначала делается дерево выражений, в котором происходит обращение к сортируемому полю. Затем из дерева выражений создается лямбда. Далее конструируется метод сортировки, способный принять лямбду. И, в конце концов, этот метод динамически запускается на выполнение.
    Самым сложным моментом здесь оказывается динамическое создание шаблонного метода, что вынесено в отдельный метод расширения GetGenericMethod.
    public static MethodInfo GetGenericMethod(
        this Type type,
        string name,
        Type[] genericTypeArgs,
        Type[] paramTypes
        )
    {
        var methods =
            //выбираем у типа все методы
            from abstractGenericMethod in type.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
            //фильтруем по имени
            where abstractGenericMethod.Name == name
            //интересуют только generic-методы
            where abstractGenericMethod.IsGenericMethod
            //выбираем параметры, которые принимает метод
            let pa = abstractGenericMethod.GetParameters()
            //отбрасываем методы принимающие иное количество параметров
            where pa.Length == paramTypes.Length
            //создаем конкретный метод, указывая типы шаблона
            select abstractGenericMethod.MakeGenericMethod(genericTypeArgs) into concreteGenericMethod
            //у созданого метода проверяем типы параметров, чтобы имеющиеся у нас типы могли быть назначены параметрам метода
            where concreteGenericMethod.GetParameters().Select(p => p.ParameterType).SequenceEqual(paramTypes, new TestAssignable())
            select concreteGenericMethod;
        //выбираем первый удовлетворяющий всем условиям метод. 
        return methods.FirstOrDefault();
    }
    

    Здесь для метод name создается у класса type, при этом имеется 2 списка типов. Список genericTypeArgs указавает для каких типов должен быть создан универсальный метод, а paramTypes показывает параметры каких типов должен принимать этот метод. Все дело в перегрузке, иногда метод может быть с разными сигнатурами, и нам нужно выбрать правильную. Поиск идет не вполне по правилам c# разрешения перегрузок, лишь принимается во внимание, чтобы можно было присвоить переданные значения аргументам метода. И затем, не долго, думая берется первая удовлетворяющая условиям перегрузка. Сравнение типов на возможность присвоения значения одного типа другому выполняется специальным классом TestAssignable.
    private class TestAssignable : IEqualityComparer<Type>
    {
        public bool Equals(Type x, Type y)
        {
            //Если тип значение типа y может использовано в качестве значения типа x, то нас это устраивает
            return x.IsAssignableFrom(y);
        }
        public int GetHashCode(Type obj)
        {
            return obj.GetHashCode();
        }
    }
    В результате можно писать такой код:
    var context = new Models.TestContext();
    var query = from customer in context.Customers
                select customer;
    
    query = query
        .ApplyOrder("name", "OrderBy")
        .ApplyOrder("surname", "ThenBy")
        .ApplyOrder("id", "ThenByDescending");
                    
    var data = query.ToList();
    

    Показаный подход с минимальными измнениями можно адаптировать к IEnumerable<> для работы с Linq to objects.
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 10
      0
      Используйте habracut, очень прошу)
        –5
        habracut? Не, не слышал.
          +1
          То ли лыжи не едут… Поставил habracut text="Подробности" /> а оно не режется.
            0
            Попробуйте так:
            <habracut text="Подробности" >
            Текст поста под катом.
            </habracut>
            
          0
          //Находим в классе Queryable метод с именем methodName. Магия поиска и создания шаблонного метода чуть ниже
              var method = typeof(Queryable).GetGenericMethod(
                  methodName,
                  //Типы аргументов шаблона метода
                  new[] { typeof(T), expr.Type },
                  //Типы параметров метода
                  new[] { source.GetType(), lambda.GetType() }
                  );
              //Выполняем метод, передавая ему неявный параметр this и лямбду. 
              //Т.е. выполняем source.OrderBy(x => x.property);
              return (IOrderedQueryable<T>)method.Invoke(null, new object[] { source, lambda });
          }
          
          
          Кхм, а зачем? Можно же вызвать напрямую, информация о типа вся есть, у вас метод сам шаблонный.
            0
            Не получится, так как неизвестен тип поля, к которому обращаемся в лямбде.
            +3
            Это всё можно немного упростить:

                public static class CustomQueryableExtensions {
                    public static IOrderedQueryable<TSource> OrderBy<TSource>(this IQueryable<TSource> source, string propName) {
                        var type = typeof (TSource);
                        var propertyType = type.GetProperty(propName).PropertyType;
                        var param = Expression.Parameter(type);
                        var keySelector = Expression.Lambda(Expression.PropertyOrField(param, propName), new[] {param});
                        return (IOrderedQueryable<TSource>) source.Provider.CreateQuery<TSource>(Expression.Call(
                            typeof (Queryable),
                            "OrderBy",
                            new[] {type, propertyType},
                            new[] {source.Expression, keySelector}));
                    }
                }
              +2
              Можно не изобретать велосипед самому — Dynamic Linq.
                0
                Да, ток увидел первые примеры сразу подумал о этой библиотеке.
                  0
                  Вот ведь! все уже придумано до нас :-)

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

                Самое читаемое