Zapytanie na podstawie stanu czasu wykonywania

W większości zapytań LINQ ogólny kształt zapytania jest ustawiany w kodzie. Elementy można filtrować przy użyciu where klauzuli , sortować kolekcję danych wyjściowych przy użyciu orderbyelementów grupowych lub wykonywać pewne obliczenia. Kod może zawierać parametry filtru lub klucza sortowania lub innych wyrażeń będących częścią zapytania. Jednak ogólny kształt zapytania nie może ulec zmianie. W tym artykule przedstawiono techniki używania System.Linq.IQueryable<T> interfejsu i typów, które implementują go w celu zmodyfikowania kształtu zapytania w czasie wykonywania.

Te techniki służą do kompilowania zapytań w czasie wykonywania, gdzie niektóre dane wejściowe lub stan czasu wykonywania zmieniają metody zapytań, których chcesz użyć w ramach zapytania. Chcesz edytować zapytanie, dodając, usuwając lub modyfikując klauzule zapytania.

Uwaga

Upewnij się, że dodasz using System.Linq.Expressions; plik .cs i using static System.Linq.Expressions.Expression; w górnej części pliku.

Rozważ użycie kodu definiującego IQueryable element lub IQueryable<T> względem źródła danych:

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);

Za każdym razem, gdy uruchamiasz poprzedni kod, jest wykonywane dokładnie to samo zapytanie. Dowiedzmy się, jak zmodyfikować zapytanie, rozszerzać je lub modyfikować. Zasadniczo element IQueryable ma dwa składniki:

  • Expression— niezależna od języka i niezależna od źródła danych reprezentacja składników bieżącego zapytania w postaci drzewa wyrażeń.
  • Provider— wystąpienie dostawcy LINQ, które wie, jak zmaterializować bieżące zapytanie w wartość lub zestaw wartości.

W kontekście dynamicznego wykonywania zapytań dostawca zwykle pozostaje taki sam; drzewo wyrażeń zapytania różni się od zapytania do zapytania.

Drzewa wyrażeń są niezmienne; jeśli chcesz użyć innego drzewa wyrażeń , a tym samym innego zapytania, musisz przetłumaczyć istniejące drzewo wyrażeń na nowe. W poniższych sekcjach opisano konkretne techniki wykonywania różnych zapytań w odpowiedzi na stan czasu wykonywania:

  • Używanie stanu czasu wykonywania z drzewa wyrażeń
  • Wywoływanie większej liczby metod LINQ
  • Zmienia drzewo wyrażeń przekazane do metod LINQ
  • Konstruowanie Expression<TDelegate> drzewa wyrażeń przy użyciu metod fabrycznych w Expression
  • Dodawanie węzłów wywołania metody do IQueryabledrzewa wyrażeń
  • Konstruowanie ciągów i używanie dynamicznej biblioteki LINQ

Każda z technik zapewnia więcej możliwości, ale kosztem zwiększonej złożoności.

Używanie stanu czasu wykonywania z drzewa wyrażeń

Najprostszym sposobem na dynamiczne wykonywanie zapytań jest odwołanie się do stanu czasu wykonywania bezpośrednio w zapytaniu za pośrednictwem zmiennej zamkniętej, takiej jak length w poniższym przykładzie kodu:

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

Drzewo wyrażeń wewnętrznych — w związku z tym zapytanie — nie jest modyfikowane; zapytanie zwraca różne wartości tylko dlatego, że wartość została zmieniona length .

Wywoływanie większej liczby metod LINQ

Ogólnie rzecz biorąc, wbudowane metody LINQ w Queryable wykonaniu dwóch kroków:

  • Zawijaj bieżące drzewo wyrażeń w MethodCallExpression obiekcie reprezentującym wywołanie metody.
  • Przekaż opakowane drzewo wyrażeń z powrotem do dostawcy, aby zwrócić wartość za pośrednictwem metody dostawcy IQueryProvider.Execute lub zwrócić przetłumaczony obiekt zapytania za pośrednictwem IQueryProvider.CreateQuery metody .

Możesz zastąpić oryginalne zapytanie wynikiem System.Linq.IQueryable<T>metody -returning, aby uzyskać nowe zapytanie. Możesz użyć stanu czasu wykonywania, jak w poniższym przykładzie:

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

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

Zmienia drzewo wyrażeń przekazane do metod LINQ

W zależności od stanu czasu wykonywania można przekazać różne wyrażenia do metod 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);

Możesz również utworzyć różne podwyrażenia przy użyciu innej biblioteki, takiej jak PredykateBuilder 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);

Konstruowanie drzew wyrażeń i zapytań przy użyciu metod fabrycznych

We wszystkich przykładach do tego momentu znasz typ elementu w czasie kompilacji —string a tym samym typ zapytania —IQueryable<string> . Możesz dodać składniki do zapytania dowolnego typu elementu lub dodać różne składniki w zależności od typu elementu. Drzewa wyrażeń można tworzyć od podstaw przy użyciu metod fabrycznych w System.Linq.Expressions.Expressionlokalizacji , a tym samym dostosować wyrażenie w czasie wykonywania do określonego typu elementu.

Konstruowanie wyrażenia<TDelegate>

Podczas konstruowania wyrażenia, które ma być przekazywane do jednej z metod LINQ, faktycznie tworzysz wystąpienie , gdzie TDelegate jest jakiś typ delegataSystem.Linq.Expressions.Expression<TDelegate>, taki jak Func<string, bool>, Actionlub niestandardowy typ delegata.

System.Linq.Expressions.Expression<TDelegate> dziedziczy z LambdaExpressionelementu , który reprezentuje pełne wyrażenie lambda, jak w poniższym przykładzie:

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

Element ma LambdaExpression dwa składniki:

  1. Lista parametrów —(string x) reprezentowana Parameters przez właściwość .
  2. Treść —x.StartsWith("a") reprezentowana Body przez właściwość .

Podstawowe kroki tworzenia elementu Expression<TDelegate> są następujące:

  1. Zdefiniuj ParameterExpression obiekty dla każdego z parametrów (jeśli istnieją) w wyrażeniu lambda przy użyciu Parameter metody factory.
    ParameterExpression x = Parameter(typeof(string), "x");
    
  2. Skonstruuj treść LambdaExpressionobiektu , używając zdefiniowanych ParameterExpression metod i metod fabrycznych pod adresem Expression. Na przykład wyrażenie reprezentujące x.StartsWith("a") może być skonstruowane w następujący sposób:
    Expression body = Call(
        x,
        typeof(string).GetMethod("StartsWith", [typeof(string)])!,
        Constant("a")
    );
    
  3. Zawijaj parametry i treść w typie czasu <kompilacji wyrażenie TDelegate> przy użyciu odpowiedniego Lambda przeciążenia metody fabryki:
    Expression<Func<string, bool>> expr = Lambda<Func<string, bool>>(body, x);
    

W poniższych sekcjach opisano scenariusz, w którym można utworzyć obiekt Expression<TDelegate> do przekazania do metody LINQ. Zawiera kompletny przykład sposobu, w jaki należy to zrobić przy użyciu metod fabrycznych.

Konstruowanie pełnego zapytania w czasie wykonywania

Chcesz pisać zapytania, które działają z wieloma typami jednostek:

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

W przypadku dowolnego z tych typów jednostek chcesz filtrować i zwracać tylko te jednostki, które mają dany tekst w jednym z pól string . W przypadku Personelementu należy wyszukać FirstName właściwości i LastName :

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

Jednak w przypadku Carelementu należy wyszukać tylko Model właściwość :

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

Chociaż można napisać jedną funkcję niestandardową dla IQueryable<Person> i drugą dla IQueryable<Car>, następująca funkcja dodaje to filtrowanie do dowolnego istniejącego zapytania, niezależnie od określonego typu elementu.

// 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 Ponieważ funkcja przyjmuje i zwraca element IQueryable<T> (a nie tylko IQueryableelement ), możesz dodać kolejne elementy zapytania w czasie kompilowania po filtrze tekstowym.

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);

Dodawanie węzłów wywołania metody do drzewa wyrażeń elementu IQueryable<TDelegate>

Jeśli masz IQueryable zamiast IQueryable<T>klasy , nie można bezpośrednio wywołać ogólnych metod LINQ. Jedną z alternatyw jest utworzenie drzewa wyrażeń wewnętrznych, jak pokazano w poprzednim przykładzie, i użycie odbicia w celu wywołania odpowiedniej metody LINQ podczas przekazywania drzewa wyrażeń.

Można również zduplikować funkcjonalność metody LINQ, opakowując całe drzewo w obiekcie MethodCallExpression reprezentującym wywołanie metody 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);
}

W takim przypadku nie masz ogólnego symbolu zastępczego czasu T kompilacji, dlatego należy użyć Lambda przeciążenia, które nie wymaga informacji o typie czasu kompilacji i które generuje wartość LambdaExpression zamiast Expression<TDelegate>.

Dynamiczna biblioteka LINQ

Konstruowanie drzew wyrażeń przy użyciu metod fabrycznych jest stosunkowo złożone; Łatwiej jest tworzyć ciągi. Dynamiczna biblioteka LINQ uwidacznia zestaw metod rozszerzeń odpowiadających standardowym metodom IQueryable LINQ w Queryablelokalizacji , i który akceptuje ciągi w specjalnej składni zamiast drzew wyrażeń. Biblioteka generuje odpowiednie drzewo wyrażeń z ciągu i może zwrócić wynikowy przetłumaczony IQueryableelement .

Na przykład poprzedni przykład może zostać przepisany w następujący sposób:

// 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);
}