Dela via


Fråga baserat på körningstillstånd

I de flesta LINQ-frågor anges frågans allmänna form i kod. Du kan filtrera objekt med hjälp av en where sats, sortera utdatasamlingen med hjälp av orderby, gruppera objekt eller utföra viss beräkning. Koden kan innehålla parametrar för filtret, sorteringsnyckeln eller andra uttryck som ingår i frågan. Frågans övergripande form kan dock inte ändras. I den här artikeln får du lära dig tekniker för att använda System.Linq.IQueryable<T> gränssnitt och typer som implementerar det för att ändra formen på en fråga vid körning.

Du använder dessa tekniker för att skapa frågor vid körning, där vissa användarindata eller körningstillstånd ändrar de frågemetoder som du vill använda som en del av frågan. Du vill redigera frågan genom att lägga till, ta bort eller ändra frågesatser.

Kommentar

Se till att du lägger till using System.Linq.Expressions; och using static System.Linq.Expressions.Expression; överst i filen .cs .

Överväg kod som definierar en IQueryable eller en IQueryable<T> mot en datakälla:

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

Varje gång du kör föregående kod körs samma exakta fråga. Nu ska vi lära oss hur du ändrar frågan utökar den eller ändrar den. I grunden har en IQueryable två komponenter:

  • Expression– en språkagnostisk och datakälla-agnostisk representation av den aktuella frågans komponenter i form av ett uttrycksträd.
  • Provider– en instans av en LINQ-provider som vet hur den aktuella frågan ska materialiseras till ett värde eller en uppsättning värden.

I samband med dynamiska frågor förblir providern vanligtvis densamma. frågeuttrycksträdet skiljer sig från fråga till fråga.

Uttrycksträd är oföränderliga. Om du vill ha ett annat uttrycksträd – och därmed en annan fråga – måste du översätta det befintliga uttrycksträdet till ett nytt. I följande avsnitt beskrivs specifika tekniker för att fråga på olika sätt som svar på körningstillstånd:

  • Använda körningstillstånd inifrån uttrycksträdet
  • Anropa fler LINQ-metoder
  • Variera uttrycksträdet som skickas till LINQ-metoderna
  • Skapa ett Expression<TDelegate> uttrycksträd med hjälp av fabriksmetoderna på Expression
  • Lägga till metodanropsnoder i ett IQueryableuttrycksträd
  • Skapa strängar och använd det dynamiska LINQ-biblioteket

Var och en av teknikerna möjliggör fler funktioner, men till en kostnad av ökad komplexitet.

Använda körningstillstånd inifrån uttrycksträdet

Det enklaste sättet att fråga dynamiskt är att referera till körningstillståndet direkt i frågan via en sluten variabel, till exempel length i följande kodexempel:

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

Det interna uttrycksträdet – och därmed frågan – ändras inte. frågan returnerar olika värden bara på grund av att värdet har ändrats length .

Anropa fler LINQ-metoder

I allmänhet utför de inbyggda LINQ-metoderna Queryable två steg:

Du kan ersätta den ursprungliga frågan med resultatet av en System.Linq.IQueryable<T>-returning-metod för att hämta en ny fråga. Du kan använda körningstillstånd, som i följande exempel:

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

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

Variera uttrycksträdet som skickas till LINQ-metoderna

Du kan skicka in olika uttryck till LINQ-metoderna, beroende på körningstillstånd:

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

Du kanske också vill skriva de olika underuttrycken med hjälp av ett annat bibliotek, till exempel LinqKits PredicateBuilder:

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

Konstruera uttrycksträd och frågor med hjälp av fabriksmetoder

I alla exempel hittills känner du till elementtypen vid kompileringstiden –string och därmed frågans typ –IQueryable<string> . Du kan lägga till komponenter i en fråga av valfri elementtyp eller lägga till olika komponenter, beroende på elementtyp. Du kan skapa uttrycksträd från grunden med hjälp av fabriksmetoderna på System.Linq.Expressions.Expressionoch på så sätt anpassa uttrycket vid körning till en viss elementtyp.

Konstruera ett uttrycks-TDelegate<>

När du skapar ett uttryck för att överföra till någon av LINQ-metoderna skapar du faktiskt en instans av System.Linq.Expressions.Expression<TDelegate>, där TDelegate är någon delegattyp som Func<string, bool>, Actioneller en anpassad delegattyp.

System.Linq.Expressions.Expression<TDelegate> ärver från LambdaExpression, som representerar ett fullständigt lambda-uttryck som i följande exempel:

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

A LambdaExpression har två komponenter:

  1. En parameterlista –(string x) som representeras av egenskapen Parameters .
  2. En brödtext –x.StartsWith("a") representerad av egenskapen Body .

De grundläggande stegen i att konstruera en Expression<TDelegate> är följande:

  1. Definiera ParameterExpression objekt för var och en av parametrarna (om några) i lambda-uttrycket med hjälp Parameter av fabriksmetoden.
    ParameterExpression x = Parameter(typeof(string), "x");
    
  2. Konstruera brödtexten i , LambdaExpressionmed hjälp av de ParameterExpression definierade och fabriksmetoderna på Expression. Ett uttryck som representerar x.StartsWith("a") kan till exempel konstrueras så här:
    Expression body = Call(
        x,
        typeof(string).GetMethod("StartsWith", [typeof(string)])!,
        Constant("a")
    );
    
  3. Omslut parametrarna och brödtexten i ett kompilerat tidstypat uttryck<TDelegate> med lämplig Lambda överlagring av fabriksmetoden:
    Expression<Func<string, bool>> expr = Lambda<Func<string, bool>>(body, x);
    

I följande avsnitt beskrivs ett scenario där du kanske vill skapa en Expression<TDelegate> för att överföra till en LINQ-metod. Det ger ett fullständigt exempel på hur du gör det med hjälp av fabriksmetoderna.

Skapa en fullständig fråga vid körning

Du vill skriva frågor som fungerar med flera entitetstyper:

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

För någon av dessa entitetstyper vill du bara filtrera och returnera de entiteter som har en viss text i ett av deras string fält. För Personvill du söka i FirstName egenskaperna och LastName :

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

Men för Carvill du bara Model söka efter egenskapen:

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

Du kan skriva en anpassad funktion för IQueryable<Person> och en annan för IQueryable<Car>, men följande funktion lägger till den här filtreringen i alla befintliga frågor, oavsett vilken typ av element som är specifik.

// 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 Eftersom funktionen tar och returnerar ett IQueryable<T> (och inte bara ett IQueryable) kan du lägga till ytterligare kompilerade frågeelement efter textfiltret.

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

Lägga till metodanropsnoder i IQueryable<TDelegates> uttrycksträd

Om du har en IQueryable i stället för en IQueryable<T>kan du inte anropa de allmänna LINQ-metoderna direkt. Ett alternativ är att skapa det inre uttrycksträdet enligt föregående exempel och använda reflektion för att anropa lämplig LINQ-metod när du skickar in uttrycksträdet.

Du kan också duplicera LINQ-metodens funktioner genom att omsluta hela trädet i ett MethodCallExpression som representerar ett anrop till LINQ-metoden:

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

I det här fallet har du ingen allmän platshållare för kompileringstid T , så du använder den Lambda överlagring som inte kräver information av kompileringstyp och som genererar en LambdaExpression i stället för en Expression<TDelegate>.

Det dynamiska LINQ-biblioteket

Det är relativt komplext att konstruera uttrycksträd med hjälp av fabriksmetoder. det är enklare att skriva strängar. Det dynamiska LINQ-biblioteket exponerar en uppsättning tilläggsmetoder som IQueryable motsvarar linq-standardmetoderna i Queryable, och som accepterar strängar i en särskild syntax i stället för uttrycksträd. Biblioteket genererar lämpligt uttrycksträd från strängen och kan returnera den resulterande översatta IQueryable.

Det tidigare exemplet kan till exempel skrivas om på följande sätt:

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