Megosztás a következőn keresztül:


Lekérdezés futásidejű állapot alapján

A legtöbb LINQ-lekérdezésben a lekérdezés általános alakja kódban van beállítva. Az elemeket záradékkal where szűrheti, rendezheti a kimeneti gyűjteményt orderby, csoportosíthatja az elemeket, vagy elvégezhet némi számítást. Előfordulhat, hogy a kód paramétereket ad a szűrőhöz, a rendezési kulcshoz vagy a lekérdezés részét képező egyéb kifejezésekhez. A lekérdezés általános alakja azonban nem módosítható. Ebben a cikkben megtanulhatja, hogyan használhat System.Linq.IQueryable<T> felületeket és olyan típusokat, amelyek implementálják a lekérdezés alakjának futásidőben történő módosításához.

Ezekkel a technikákkal futtatáskor hozhat létre lekérdezéseket, ahol egyes felhasználói bemeneti vagy futásidejű állapotok módosítják a lekérdezés részeként használni kívánt lekérdezési módszereket. A lekérdezést lekérdezési záradékok hozzáadásával, eltávolításával vagy módosításával szeretné szerkeszteni.

Feljegyzés

Győződjön meg arról, hogy hozzáadja using System.Linq.Expressions; a using static System.Linq.Expressions.Expression; .cs fájl elejét.

Fontolja meg az adatforráshoz tartozó vagy az azokkal IQueryable szemben definiált IQueryable<T> kódot:

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

Minden alkalommal, amikor az előző kódot futtatja, ugyanazt a lekérdezést hajtja végre. Ebből a cikkből megtudhatja, hogyan terjesztheti ki vagy módosíthatja a lekérdezést. Alapvetően egy IQueryable két összetevőből áll:

  • Expression— az aktuális lekérdezés összetevőinek nyelvi-agnosztikus és adatforrás-agnosztikus ábrázolása kifejezésfa formájában.
  • Provider– egy LINQ-szolgáltató egy példánya, amely tudja, hogyan lehet az aktuális lekérdezést értékké vagy értékhalmazsá alakítani.

A dinamikus lekérdezések kontextusában a szolgáltató általában ugyanaz marad; a lekérdezés kifejezésfája lekérdezésenként eltér.

A kifejezésfák nem módosíthatók; ha egy másik kifejezésfát – és így egy másik lekérdezést – szeretne használni, akkor a meglévő kifejezésfát egy újra kell lefordítania. A következő szakaszok a futtatási időállapotra adott eltérő lekérdezési technikákat ismertetik:

  • Futásidejű állapot használata a kifejezésfán belülről
  • További LINQ-metódusok meghívása
  • A LINQ-metódusokba átadott kifejezésfa változója
  • Expression<TDelegate> Kifejezésfa létrehozása a következő helyen található gyári módszerekkel:Expression
  • Metódushívási csomópontok hozzáadása egy IQueryablekifejezésfához
  • Sztringek létrehozása és a Dinamikus LINQ-kódtár használata

Mindegyik technika több képességet tesz lehetővé, de a megnövekedett összetettség árán.

Futásidejű állapot használata a kifejezésfán belülről

A dinamikus lekérdezés legegyszerűbb módja, ha közvetlenül a lekérdezés futásidejű állapotára hivatkozik egy bezárt változón keresztül, például length a következő kód példájában:

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

A belső kifejezésfa – és így a lekérdezés – nem módosul; a lekérdezés csak azért ad vissza különböző értékeket, mert a változó értéke length módosult.

További LINQ-metódusok meghívása

A beépített LINQ-metódusok általában két lépést hajtanak Queryable végre:

  • Az aktuális kifejezésfa körbefuttatása a MethodCallExpression metódushívást jelképező módon.
  • Adja vissza a becsomagolt kifejezésfát a szolgáltatónak, hogy visszaadjon egy értéket a szolgáltató metódusán IQueryProvider.Execute keresztül, vagy egy lefordított lekérdezési objektumot adjon vissza a IQueryProvider.CreateQuery metóduson keresztül.

Az eredeti lekérdezést lecserélheti egy System.Linq.IQueryable<T>-returning metódus eredményére egy új lekérdezés lekéréséhez. A futásidejű állapotot az alábbi példához hasonlóan használhatja:

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

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

A LINQ-metódusokba átadott kifejezésfa változója

A linq metódusoknak különböző kifejezéseket adhat át a futási idő állapotától függően:

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

A különböző alkifejezéseket egy másik kódtár, például a LinqKit PredicateBuilder használatával is meg kell írnia:

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

Kifejezésfák és lekérdezések létrehozása gyári módszerekkel

Az addigi példákban a fordításkor ismert elemtípust –string és így a lekérdezés típusát –IQueryable<string> ismerjük. Az elemek típusától függően összetevőket adhat hozzá egy tetszőleges elemtípusú lekérdezéshez, vagy különböző összetevőket is hozzáadhat. Az alapoktól kezdve létrehozhat kifejezésfákat a gyári metódusok System.Linq.Expressions.Expressionhasználatával, és így a futtatáskor használt kifejezést egy adott elemtípushoz igazíthatja.

Kifejezés<tDelegate létrehozása>

Amikor olyan kifejezést hoz létre, amely átmegy az egyik LINQ-metódusba, valójában egy példányt System.Linq.Expressions.Expression<TDelegate>hoz létre, ahol TDelegate valamilyen delegálási típus, például Func<string, bool>, Actionvagy egy egyéni delegálttípust.

System.Linq.Expressions.Expression<TDelegate> örökli a LambdaExpressionkövetkező példához hasonló teljes lambda kifejezést:

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

Az A LambdaExpression két összetevőből áll:

  1. A tulajdonság által Parameters képviselt paraméterlista(string x).
  2. A tulajdonság által Body képviselt testx.StartsWith("a").

Az an Expression<TDelegate> felépítésének alapvető lépései a következők:

  1. A lambda kifejezésben szereplő paraméterek (ha vannak ilyenek) objektumainak definiálása ParameterExpression a Parameter gyári módszerrel.
    ParameterExpression x = Parameter(typeof(string), "x");
    
  2. Hozza létre a törzsét LambdaExpressiona ParameterExpression megadott és a gyári metódusok használatával a következő helyen Expression: . Egy kifejezés x.StartsWith("a") például a következőképpen hozható létre:
    Expression body = Call(
        x,
        typeof(string).GetMethod("StartsWith", [typeof(string)])!,
        Constant("a")
    );
    
  3. Csomagolja be a paramétereket és a törzset egy fordítási idő típusú Expression<TDelegate-be> a megfelelő Lambda gyári metódus túlterhelésével:
    Expression<Func<string, bool>> expr = Lambda<Func<string, bool>>(body, x);
    

Az alábbi szakaszok egy olyan forgatókönyvet mutatnak be, amelyben egy LINQ-metódusba való átengedést szeretne létrehozni Expression<TDelegate> . Ez egy teljes példát nyújt arra, hogyan teheti ezt meg a gyári módszerekkel.

Teljes lekérdezés létrehozása futásidőben

Több entitástípussal működő lekérdezéseket szeretne írni:

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

Ezen entitástípusok bármelyike esetében csak azokat az entitásokat szeretné szűrni és visszaadni, amelyek egy adott szöveggel rendelkeznek az egyik mezőjükben string . A következőt Personszeretné keresni:FirstNameLastName

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

CarEhhez azonban csak a Model tulajdonságban szeretne keresni:

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

Bár megírhat egy egyéni függvényt IQueryable<Person> és egy másikat IQueryable<Car>, az alábbi függvény hozzáadja ezt a szűrést bármely meglévő lekérdezéshez, függetlenül az adott elemtípustól.

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

Mivel a TextFilter függvény egy IQueryable<T> (és nem csak egy) lekérdezést IQueryablevesz fel és ad vissza, a szövegszűrő után további fordítási idő típusú lekérdezési elemeket is hozzáadhat.

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

Metódushívási csomópontok hozzáadása az IQueryable<TDelegate> kifejezésfához

Ha nem egy, hanem egy IQueryableIQueryable<T>, akkor nem hívhatja meg közvetlenül az általános LINQ metódusokat. Egy másik lehetőség a belső kifejezésfa létrehozása az előző példában látható módon, és a tükrözés használatával meghívja a megfelelő LINQ metódust a kifejezésfán való áthaladás során.

A LINQ metódus funkcióit is duplikálhatja úgy, hogy a teljes fát becsomagolja a MethodCallExpression LINQ metódus hívását jelképező körbefuttatással:

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

Ebben az esetben nem rendelkezik általános fordítási idő T típusú helyőrzővel, ezért a Lambda túlterhelést használja, amely nem igényel fordítási idő típusú adatokat, és amely egy helyett egy LambdaExpressionExpression<TDelegate>.

A Dinamikus LINQ-kódtár

A kifejezésfák létrehozása gyári módszerekkel viszonylag bonyolult; egyszerűbb sztringeket írni. A Dinamikus LINQ-kódtár a standard LINQ-metódusoknak Queryablemegfelelő bővítménymeta-metódusokat IQueryable tesz elérhetővé, amelyek kifejezésfák helyett speciális szintaxisban fogadnak el sztringeket. A kódtár létrehozza a megfelelő kifejezésfát a sztringből, és visszaadja az eredményül kapott lefordított értéket IQueryable.

Az előző példa például a következőképpen írható át:

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