Sdílet prostřednictvím


Dotaz na základě stavu za běhu

Ve většině dotazů LINQ je obecný tvar dotazu nastavený v kódu. Položky můžete filtrovat pomocí where klauzule, seřadit výstupní kolekci pomocí orderby, seskupovat položky nebo provádět určité výpočty. Kód může poskytnout parametry pro filtr nebo klíč řazení nebo jiné výrazy, které jsou součástí dotazu. Celkový tvar dotazu se ale nemůže změnit. V tomto článku se naučíte techniky použití System.Linq.IQueryable<T> rozhraní a typů, které ho implementují k úpravě tvaru dotazu za běhu.

Tyto techniky se používají k vytváření dotazů za běhu, kdy některé vstupy uživatelů nebo stavy za běhu mění metody dotazu, které chcete použít jako součást dotazu. Dotaz chcete upravit přidáním, odebráním nebo úpravou klauzulí dotazu.

Poznámka:

Nezapomeňte do horní části souboru .cs přidat a using static System.Linq.Expressions.Expression; přidat using System.Linq.Expressions; ho.

Zvažte kód, který definuje zdroj IQueryable dat nebo proti IQueryable<T> ho:

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

Pokaždé, když spustíte předchozí kód, provede se stejný přesný dotaz. Pojďme se dozvědět, jak dotaz rozšířit nebo upravit. IQueryable V zásadě má dvě komponenty:

  • Expression— nezávislá na jazyce a zdroje dat nezávislá na reprezentaci komponent aktuálního dotazu ve formě stromu výrazů.
  • Provider– instance zprostředkovatele LINQ, která ví, jak materializovat aktuální dotaz na hodnotu nebo sadu hodnot.

V kontextu dynamického dotazování zůstává poskytovatel obvykle stejný; strom výrazů dotazu se liší od dotazu po dotaz.

Stromy výrazů jsou neměnné; Pokud chcete jiný strom výrazů , a tedy jiný dotaz, musíte přeložit existující strom výrazu na nový. Následující části popisují konkrétní techniky pro dotazování odlišně v reakci na stav za běhu:

  • Použití stavu běhu ze stromu výrazů
  • Volání dalších metod LINQ
  • Různé stromy výrazů předávané do metod LINQ
  • Vytvoření stromu výrazů Expression<TDelegate> pomocí metod továrny na adrese Expression
  • Přidání uzlů volání metody do IQueryablestromu výrazů
  • Vytváření řetězců a použití dynamické knihovny LINQ

Každá z technik umožňuje více možností, ale za cenu zvýšené složitosti.

Použití stavu běhu ze stromu výrazů

Nejjednodušší způsob, jak se dynamicky dotazovat, je odkazovat na stav běhu přímo v dotazu prostřednictvím uzavřené proměnné, například length v následujícím příkladu kódu:

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

Strom interního výrazu ( a proto dotaz) není změněn; dotaz vrátí různé hodnoty pouze proto, že hodnota změněna length .

Volání dalších metod LINQ

Obecně platí, že integrované metodyQueryable LINQ provádějí dva kroky:

  • Zabalte strom aktuálního výrazu do MethodCallExpression představující volání metody.
  • Předejte zalomený strom výrazu zpět zprostředkovateli, a to buď k vrácení hodnoty prostřednictvím metody zprostředkovatele IQueryProvider.Execute , nebo k vrácení přeloženého objektu IQueryProvider.CreateQuery dotazu prostřednictvím metody.

Původní dotaz můžete nahradit výsledkem System.Linq.IQueryable<T>metody -returning, abyste získali nový dotaz. Můžete použít stav běhu, jak je znázorněno v následujícím příkladu:

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

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

Různé stromy výrazů předávané do metod LINQ

Různé výrazy můžete předávat metodám LINQ v závislosti na stavu běhu:

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

Můžete také chtít vytvořit různé dílčí výrazy pomocí jiné knihovny, jako je PredicateBuilder 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);

Vytváření stromů výrazů a dotazů pomocí metod továrny

Ve všech příkladech až do tohoto okamžiku znáte typ elementu v době kompilace –string a tím i typ dotazu.IQueryable<string> Komponenty můžete přidat do dotazu libovolného typu elementu nebo přidat různé komponenty v závislosti na typu elementu. Stromy výrazů můžete vytvářet od základů pomocí metod továrny a System.Linq.Expressions.Expressionpřizpůsobit tak výraz za běhu konkrétnímu typu prvku.

Vytvoření výrazu<TDelegate>

Když vytváříte výraz, který se má předat do jedné z metod LINQ, ve skutečnosti vytváříte instanci System.Linq.Expressions.Expression<TDelegate>, kde TDelegate je nějaký typ delegáta, například Func<string, bool>, Actionnebo vlastní typ delegáta.

System.Linq.Expressions.Expression<TDelegate> dědí z LambdaExpressionvýrazu , který představuje úplný výraz lambda jako v následujícím příkladu:

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

A LambdaExpression má dvě komponenty:

  1. Seznam(string x) parametrů – reprezentovaný Parameters vlastností.
  2. Tělo –x.StartsWith("a") reprezentované Body vlastností.

Základní kroky při vytváření jsou Expression<TDelegate> následující:

  1. Definujte ParameterExpression objekty pro každý z parametrů (pokud existuje) ve výrazu lambda pomocí Parameter metody továrny.
    ParameterExpression x = Parameter(typeof(string), "x");
    
  2. Vytvořte tělo vašeho LambdaExpression, pomocí ParameterExpression definované, a metody továrny v Expression. Například výraz představující x.StartsWith("a") by mohl být vytvořen takto:
    Expression body = Call(
        x,
        typeof(string).GetMethod("StartsWith", [typeof(string)])!,
        Constant("a")
    );
    
  3. Zabalte parametry a text do TDelegate výrazu tDelegate> s časovým typem kompilace pomocí přetížení příslušné Lambda metody továrny:<
    Expression<Func<string, bool>> expr = Lambda<Func<string, bool>>(body, x);
    

Následující části popisují scénář, ve kterém můžete chtít sestavit metodu Expression<TDelegate> LINQ. Poskytuje úplný příklad toho, jak to provést pomocí metod továrny.

Vytvoření úplného dotazu za běhu

Chcete psát dotazy, které pracují s více typy entit:

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

U některého z těchto typů entit chcete filtrovat a vracet pouze ty entity, které mají daný text uvnitř jednoho z jejich string polí. V Personpřípadě byste chtěli hledat FirstName vlastnosti a LastName vlastnosti:

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

Ale v případě Car, byste chtěli vyhledat pouze Model vlastnost:

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

I když byste mohli napsat jednu vlastní funkci pro a jinou pro IQueryable<Person>IQueryable<Car>, následující funkce přidá toto filtrování do jakéhokoli existujícího dotazu bez ohledu na konkrétní typ prvku.

// 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 Vzhledem k tomu, že funkce přebírá a vrací IQueryable<T> (a nejen), IQueryablemůžete za textový filtr přidat další prvky dotazu typu kompilace.

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

Přidání uzlů volání metody do stromu výrazů IQueryable<TDelegate>

Pokud máte IQueryable místo IQueryable<T>, nemůžete přímo volat obecné metody LINQ. Jednou z možností je vytvořit strom vnitřního výrazu, jak je znázorněno v předchozím příkladu, a použít reflexi k vyvolání příslušné metody LINQ při předávání stromu výrazů.

Můžete také duplikovat funkce metody LINQ tak, že celý strom zabalíte do objektu MethodCallExpression , který představuje volání 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);
}

V tomto případě nemáte obecný zástupný symbol pro kompilaci T , takže použijete Lambda přetížení, které nevyžaduje informace o typu kompilace a který vytváří LambdaExpression místo Expression<TDelegate>.

Dynamická knihovna LINQ

Vytváření stromů výrazů pomocí metod továrny je poměrně složité; Vytváření řetězců je jednodušší. Dynamická knihovna LINQ zveřejňuje sadu rozšiřujících metod odpovídajících IQueryable standardním metodám LINQ at Queryablea které přijímají řetězce ve speciální syntaxi místo stromů výrazů. Knihovna vygeneruje z řetězce odpovídající strom výrazů a může vrátit výslednou přeloženou .IQueryable

Například předchozí příklad by se dal přepsat následujícím způsobem:

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