Запрос на основе состояния времени выполнения

В большинстве запросов LINQ общая форма запроса устанавливается в коде. Вы можете фильтровать элементы с помощью предложения, сортировать выходную where коллекцию с помощью orderbyэлементов группы или выполнять некоторые вычисления. Код может предоставить параметры фильтра или ключа сортировки или другие выражения, которые являются частью запроса. Однако общая форма запроса не может измениться. Из этой статьи вы узнаете, как использовать System.Linq.IQueryable<T> интерфейс и типы, реализующие его для изменения формы запроса во время выполнения.

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

Примечание.

Убедитесь, что вы добавили using System.Linq.Expressions; и using static System.Linq.Expressions.Expression; в верхней части файла .cs.

Рассмотрим код, определяющий IQueryable или IQueryable<T> против источника данных:

string[] companyNames = [
    "Consolidated Messenger", "Alpine Ski House", "Southridge Video",
    "City Power & Light", "Coho Winery", "Wide World Importers",
    "Graphic Design Institute", "Adventure Works", "Humongous Insurance",
    "Woodgrove Bank", "Margie's Travel", "Northwind Traders",
    "Blue Yonder Airlines", "Trey Research", "The Phone Company",
    "Wingtip Toys", "Lucerne Publishing", "Fourth Coffee"
];

// Use an in-memory array as the data source, but the IQueryable could have come
// from anywhere -- an ORM backed by a database, a web request, or any other LINQ provider.
IQueryable<string> companyNamesSource = companyNames.AsQueryable();
var fixedQry = companyNames.OrderBy(x => x);

Каждый раз, когда выполняется предыдущий код, выполняется тот же точный запрос. Давайте узнаем, как изменить запрос или изменить его. В своей основе IQueryable имеет два компонента:

  • Expression— не зависящая от языка и источника данных представление компонентов текущего запроса в виде дерева выражений.
  • Provider— экземпляр поставщика LINQ, который знает, как материализовать текущий запрос в значение или набор значений.

В контексте динамического запроса поставщик обычно остается неизменным; Дерево выражений запроса отличается от запроса к запросу.

Деревья выражений неизменяемы; Если требуется другое дерево выражений ( и, таким образом, другой запрос), необходимо перевести существующее дерево выражений в новое. В следующих разделах описываются определенные методы запроса по-разному в ответ на состояние времени выполнения.

  • Использование состояния времени выполнения из дерева выражений
  • Вызов дополнительных методов LINQ
  • Изменение дерева выражения, переданного в методы LINQ
  • Создание дерева выражений Expression<TDelegate> с помощью методов фабрики в Expression
  • Добавление узлов вызова метода в дерево выражения IQueryable.
  • Создание строк и использование динамической библиотеки LINQ

Каждый из методов обеспечивает больше возможностей, но за счет повышения сложности.

Использование состояния времени выполнения из дерева выражений

Самый простой способ динамического запроса заключается в том, чтобы ссылаться на состояние времени выполнения непосредственно в запросе с помощью закрытой переменной, например length в следующем примере кода:

var length = 1;
var qry = companyNamesSource
    .Select(x => x.Substring(0, length))
    .Distinct();

Console.WriteLine(string.Join(",", qry));
// prints: C, A, S, W, G, H, M, N, B, T, L, F

length = 2;
Console.WriteLine(string.Join(",", qry));
// prints: Co, Al, So, Ci, Wi, Gr, Ad, Hu, Wo, Ma, No, Bl, Tr, Th, Lu, Fo

Дерево внутренних выражений (и таким образом запрос) не изменяется; Запрос возвращает разные значения, только так как значение изменено length .

Вызов дополнительных методов LINQ

Как правило, встроенные методы LINQ в Queryable выполняют два действия:

  • Заключение текущего дерево выражения в оболочку в MethodCallExpression, который представляет вызов метода.
  • Передача инкапсулированного дерева выражения в поставщик, чтобы вернуть значение через метод IQueryProvider.Execute поставщика, либо чтобы вернуть объект переведенного запроса с помощью метода IQueryProvider.CreateQuery.

Вы можете заменить исходный запрос результатом System.Linq.IQueryable<T>метода -returning, чтобы получить новый запрос. Состояние времени выполнения можно использовать, как показано в следующем примере:

// bool sortByLength = /* ... */;

var qry = companyNamesSource;
if (sortByLength)
{
    qry = qry.OrderBy(x => x.Length);
}

Изменение дерева выражения, переданного в методы LINQ

Вы можете передать различные выражения в методы LINQ в зависимости от состояния времени выполнения:

// string? startsWith = /* ... */;
// string? endsWith = /* ... */;

Expression<Func<string, bool>> expr = (startsWith, endsWith) switch
{
    ("" or null, "" or null) => x => true,
    (_, "" or null) => x => x.StartsWith(startsWith),
    ("" or null, _) => x => x.EndsWith(endsWith),
    (_, _) => x => x.StartsWith(startsWith) || x.EndsWith(endsWith)
};

var qry = companyNamesSource.Where(expr);

Кроме того, может потребоваться создать различные вложенные выражения с помощью другой библиотеки, такой как ПредикатBuilder LinqKit:

// This is functionally equivalent to the previous example.

// using LinqKit;
// string? startsWith = /* ... */;
// string? endsWith = /* ... */;

Expression<Func<string, bool>>? expr = PredicateBuilder.New<string>(false);
var original = expr;
if (!string.IsNullOrEmpty(startsWith))
{
    expr = expr.Or(x => x.StartsWith(startsWith));
}
if (!string.IsNullOrEmpty(endsWith))
{
    expr = expr.Or(x => x.EndsWith(endsWith));
}
if (expr == original)
{
    expr = x => true;
}

var qry = companyNamesSource.Where(expr);

Создание деревьев выражений и запросов с помощью фабричных методов

Во всех примерах до этого момента вы знаете тип элемента во время компиляции (string и таким образом тип запросаIQueryable<string>). Вы можете добавить компоненты в запрос любого типа элемента или добавить различные компоненты в зависимости от типа элемента. Можно создавать деревья выражений с нуля, используя фабричные методы в System.Linq.Expressions.Expression и таким образом адаптировать выражение в среде выполнения к определенному типу элемента.

Создание TDelegate выражения<>

При создании выражения для передачи в один из методов LINQ вы фактически создаете экземпляр System.Linq.Expressions.Expression<TDelegate>, где TDelegate является некоторый тип делегата, например Func<string, bool>Action, или настраиваемый тип делегата.

System.Linq.Expressions.Expression<TDelegate> наследует от LambdaExpression, представляющее полное лямбда-выражение, как показано в следующем примере:

Expression<Func<string, bool>> expr = x => x.StartsWith("a");

LambdaExpression имеет два компонента:

  1. Список параметров,(string x) представленный свойством Parameters .
  2. Тело,x.StartsWith("a") представленное свойством Body .

Ниже приведены основные шаги по созданию объекта Expression<TDelegate> :

  1. Определите объекты ParameterExpression для каждого из параметров (если таковые имеются) в лямбда-выражении с помощью фабричного метода Parameter.
    ParameterExpression x = Parameter(typeof(string), "x");
    
  2. Создайте текст объектаLambdaExpression, используя ParameterExpression определенные и заводские методы.Expression Например, выражение, представляющее x.StartsWith("a"), может быть построено следующим образом:
    Expression body = Call(
        x,
        typeof(string).GetMethod("StartsWith", [typeof(string)])!,
        Constant("a")
    );
    
  3. Обтекайте параметры и текст в типизированном <выражении времени компиляции TDelegate>, используя соответствующую Lambda перегрузку метода фабрики:
    Expression<Func<string, bool>> expr = Lambda<Func<string, bool>>(body, x);
    

В следующих разделах описывается сценарий, в котором может потребоваться создать метод Expression<TDelegate> LINQ. Он содержит полный пример того, как это сделать с помощью методов фабрики.

Создание полного запроса во время выполнения

Вы хотите написать запросы, которые работают с несколькими типами сущностей:

record Person(string LastName, string FirstName, DateTime DateOfBirth);
record Car(string Model, int Year);

Для любого из этих типов сущностей необходимо отфильтровать и возвратить только те сущности, которые имеют заданный текст в одном из полей string. Для Person необходимо найти свойства FirstName и LastName:

string term = /* ... */;
var personsQry = new List<Person>()
    .AsQueryable()
    .Where(x => x.FirstName.Contains(term) || x.LastName.Contains(term));

Но для Car требуется найти только свойство Model:

string term = /* ... */;
var carsQry = new List<Car>()
    .AsQueryable()
    .Where(x => x.Model.Contains(term));

Несмотря на то что можно написать одну настраиваемую функцию для IQueryable<Person> и другую для IQueryable<Car>, следующая функция добавляет эту фильтрацию в любой существующий запрос независимо от конкретного типа элемента.

// using static System.Linq.Expressions.Expression;

IQueryable<T> TextFilter<T>(IQueryable<T> source, string term)
{
    if (string.IsNullOrEmpty(term)) { return source; }

    // T is a compile-time placeholder for the element type of the query.
    Type elementType = typeof(T);

    // Get all the string properties on this specific type.
    PropertyInfo[] stringProperties = elementType
        .GetProperties()
        .Where(x => x.PropertyType == typeof(string))
        .ToArray();
    if (!stringProperties.Any()) { return source; }

    // Get the right overload of String.Contains
    MethodInfo containsMethod = typeof(string).GetMethod("Contains", [typeof(string)])!;

    // Create a parameter for the expression tree:
    // the 'x' in 'x => x.PropertyName.Contains("term")'
    // The type of this parameter is the query's element type
    ParameterExpression prm = Parameter(elementType);

    // Map each property to an expression tree node
    IEnumerable<Expression> expressions = stringProperties
        .Select(prp =>
            // For each property, we have to construct an expression tree node like x.PropertyName.Contains("term")
            Call(                  // .Contains(...) 
                Property(          // .PropertyName
                    prm,           // x 
                    prp
                ),
                containsMethod,
                Constant(term)     // "term" 
            )
        );

    // Combine all the resultant expression nodes using ||
    Expression body = expressions
        .Aggregate((prev, current) => Or(prev, current));

    // Wrap the expression body in a compile-time-typed lambda expression
    Expression<Func<T, bool>> lambda = Lambda<Func<T, bool>>(body, prm);

    // Because the lambda is compile-time-typed (albeit with a generic parameter), we can use it with the Where method
    return source.Where(lambda);
}

TextFilter Так как функция принимает и возвращает IQueryable<T> (а не толькоIQueryable), вы можете добавить дополнительные элементы запроса во время компиляции после текстового фильтра.

var qry = TextFilter(
        new List<Person>().AsQueryable(),
        "abcd"
    )
    .Where(x => x.DateOfBirth < new DateTime(2001, 1, 1));

var qry1 = TextFilter(
        new List<Car>().AsQueryable(),
        "abcd"
    )
    .Where(x => x.Year == 2010);

Добавление узлов вызова метода в дерево выражений IQueryable<TDelegate>

Если у вас есть IQueryable вместо нее IQueryable<T>, вы не можете напрямую вызывать универсальные методы LINQ. Одним из вариантов является создание дерева внутренних выражений, как показано в предыдущем примере, и использование отражения для вызова соответствующего метода LINQ во время передачи в дерево выражений.

Можно также дублировать функциональность метода LINQ, заключив все дерево в MethodCallExpression, который представляет вызов метода LINQ:

IQueryable TextFilter_Untyped(IQueryable source, string term)
{
    if (string.IsNullOrEmpty(term)) { return source; }
    Type elementType = source.ElementType;

    // The logic for building the ParameterExpression and the LambdaExpression's body is the same as in the previous example,
    // but has been refactored into the constructBody function.
    (Expression? body, ParameterExpression? prm) = constructBody(elementType, term);
    if (body is null) { return source; }

    Expression filteredTree = Call(
        typeof(Queryable),
        "Where",
        [elementType],
        source.Expression,
        Lambda(body, prm!)
    );

    return source.Provider.CreateQuery(filteredTree);
}

В этом случае у вас нет универсального заполнителя времени T компиляции, поэтому вы используете Lambda перегрузку, которая не требует сведений о типе компиляции, и которая создает LambdaExpression вместо него Expression<TDelegate>.

Динамическая библиотека LINQ

Создание деревьев выражений с помощью методов фабрики является относительно сложным; проще создавать строки. Динамическая библиотека LINQ предоставляет набор методов расширения для IQueryable, соответствующих стандартным методам LINQ в Queryable, и который принимает строки в специальном синтаксисе вместо деревьев выражений. Библиотека создает соответствующее дерево выражения из строки и может возвращать результирующий преобразованный IQueryable.

Например, предыдущий пример можно переписать следующим образом:

// using System.Linq.Dynamic.Core

IQueryable TextFilter_Strings(IQueryable source, string term)
{
    if (string.IsNullOrEmpty(term)) { return source; }

    var elementType = source.ElementType;

    // Get all the string property names on this specific type.
    var stringProperties =
        elementType.GetProperties()
            .Where(x => x.PropertyType == typeof(string))
            .ToArray();
    if (!stringProperties.Any()) { return source; }

    // Build the string expression
    string filterExpr = string.Join(
        " || ",
        stringProperties.Select(prp => $"{prp.Name}.Contains(@0)")
    );

    return source.Where(filterExpr, term);
}