Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 18.06.2014 | нет комментариев
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;
    //выбираем 1-й удовлетворяющий каждому условиям способ. 
    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.

 

Источник: programmingmaster.ru

 

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB