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

в 8:01, , рубрики: generics, linq, Visual Studio, Программирование, метки: , ,

Введение

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.

Автор: drBasic

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js