Delen via


Query op basis van runtimestatus

In de meeste LINQ-query's wordt de algemene vorm van de query ingesteld in code. U kunt items filteren met behulp van een where component, de uitvoerverzameling sorteren met behulp orderbyvan, groepeer items of een berekening uitvoeren. Uw code bevat mogelijk parameters voor het filter of de sorteersleutel of andere expressies die deel uitmaken van de query. De algehele vorm van de query kan echter niet worden gewijzigd. In dit artikel leert u technieken voor het gebruik System.Linq.IQueryable<T> van interface en typen die deze implementeren om de vorm van een query tijdens runtime te wijzigen.

U gebruikt deze technieken om query's tijdens runtime te bouwen, waarbij sommige gebruikersinvoer of runtimestatus de querymethoden wijzigt die u wilt gebruiken als onderdeel van de query. U wilt de query bewerken door querycomponenten toe te voegen, te verwijderen of te wijzigen.

Notitie

Zorg ervoor dat u het .cs bestand toevoegt using System.Linq.Expressions; en using static System.Linq.Expressions.Expression; boven aan het bestand.

Houd rekening met code waarmee een IQueryable of een IQueryable<T> gegevensbron wordt gedefinieerd:

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

Telkens wanneer u de voorgaande code uitvoert, wordt dezelfde exacte query uitgevoerd. Laten we eens leren hoe u de query kunt uitbreiden of wijzigen. Een heeft in principe IQueryable twee onderdelen:

  • Expression— een taalagnostische en gegevensbronagnostische weergave van de onderdelen van de huidige query, in de vorm van een expressiestructuur.
  • Provider— een exemplaar van een LINQ-provider, die weet hoe de huidige query moet worden gerealiseerd in een waarde of set waarden.

In de context van dynamische query's blijft de provider meestal hetzelfde; de expressiestructuur van de query verschilt van query tot query.

Expressiestructuren zijn onveranderbaar; Als u een andere expressiestructuur en dus een andere query wilt, moet u de bestaande expressiestructuur vertalen naar een nieuwe. In de volgende secties worden specifieke technieken beschreven voor het opvragen van query's op een andere manier als reactie op runtimestatus:

  • Runtimestatus gebruiken vanuit de expressiestructuur
  • Meer LINQ-methoden aanroepen
  • De expressiestructuur variëren die is doorgegeven aan de LINQ-methoden
  • Expression<TDelegate> Een expressiestructuur maken met behulp van de factory-methoden opExpression
  • Methodeaanroepknooppunten toevoegen aan een IQueryableexpressiestructuur
  • Tekenreeksen maken en de dynamische LINQ-bibliotheek gebruiken

Elk van de technieken maakt meer mogelijkheden mogelijk, maar met een hogere complexiteit.

Runtimestatus gebruiken vanuit de expressiestructuur

De eenvoudigste manier om dynamisch query's uit te voeren, is door rechtstreeks in de query te verwijzen naar de runtimestatus via een closed-over-variabele, zoals length in het volgende codevoorbeeld:

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

De interne expressiestructuur, en dus de query, wordt niet gewijzigd; de query retourneert alleen verschillende waarden omdat de waarde van length gewijzigd.

Meer LINQ-methoden aanroepen

Over het algemeen voeren de ingebouwde LINQ-methodenQueryable bij het uitvoeren van twee stappen uit:

  • Verpakt de huidige expressiestructuur in een MethodCallExpression weergave van de methode-aanroep.
  • Geef de structuur van de verpakte expressie weer door aan de provider om een waarde te retourneren via de methode van IQueryProvider.Execute de provider of om een vertaald queryobject via de IQueryProvider.CreateQuery methode te retourneren.

U kunt de oorspronkelijke query vervangen door het resultaat van een System.Linq.IQueryable<T>retourmethode om een nieuwe query op te halen. U kunt runtimestatus gebruiken, zoals in het volgende voorbeeld:

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

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

De expressiestructuur variëren die is doorgegeven aan de LINQ-methoden

U kunt verschillende expressies doorgeven aan de LINQ-methoden, afhankelijk van de runtimestatus:

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

U kunt ook de verschillende subexpressies opstellen met behulp van een andere bibliotheek, zoals PredicateBuilder van 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);

Expressiestructuren en query's maken met behulp van factory-methoden

In alle voorbeelden tot nu toe kent u het elementtype tijdens het compileren(string en dus het type query).IQueryable<string> U kunt onderdelen toevoegen aan een query van elk elementtype of verschillende onderdelen toevoegen, afhankelijk van het elementtype. U kunt expressiestructuren vanaf de grond maken met behulp van de fabrieksmethoden op System.Linq.Expressions.Expression, en zo de expressie tijdens runtime aanpassen aan een specifiek elementtype.

Een expressie-TDelegate<maken>

Wanneer u een expressie maakt die moet worden doorgegeven aan een van de LINQ-methoden, maakt u eigenlijk een exemplaar van System.Linq.Expressions.Expression<TDelegate>, waarbij TDelegate een bepaald type gedelegeerde is, zoals Func<string, bool>, Actionof een aangepast gemachtigdetype.

System.Linq.Expressions.Expression<TDelegate> neemt over van LambdaExpression, dat een volledige lambda-expressie vertegenwoordigt, zoals in het volgende voorbeeld:

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

A LambdaExpression heeft twee onderdelen:

  1. Een lijst met parameters,(string x) vertegenwoordigd door de Parameters eigenschap.
  2. Een hoofdtekst,x.StartsWith("a") vertegenwoordigd door de Body eigenschap.

De basisstappen voor het samenstellen van een zijn Expression<TDelegate> als volgt:

  1. Definieer ParameterExpression objecten voor elk van de parameters (indien aanwezig) in de lambda-expressie, met behulp van de Parameter factory-methode.
    ParameterExpression x = Parameter(typeof(string), "x");
    
  2. Bouw de hoofdtekst van uw LambdaExpression, met behulp van de ParameterExpression gedefinieerde en de fabrieksmethoden bij Expression. Een expressie die wordt weergegeven x.StartsWith("a") , kan bijvoorbeeld als volgt worden samengesteld:
    Expression body = Call(
        x,
        typeof(string).GetMethod("StartsWith", [typeof(string)])!,
        Constant("a")
    );
    
  3. Wrap the parameters and body in a compile-time-typed Expression<TDelegate>, using the appropriate Lambda factory method overload:
    Expression<Func<string, bool>> expr = Lambda<Func<string, bool>>(body, x);
    

In de volgende secties wordt een scenario beschreven waarin u een scenario wilt maken Expression<TDelegate> dat moet worden doorgegeven aan een LINQ-methode. Het biedt een volledig voorbeeld van hoe u dit doet met behulp van de factory-methoden.

Een volledige query maken tijdens runtime

U wilt query's schrijven die werken met meerdere entiteitstypen:

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

Voor een van deze entiteitstypen wilt u alleen de entiteiten filteren en retourneren die een bepaalde tekst in een van hun string velden hebben. Voor Person, wilt u zoeken in de FirstName en LastName eigenschappen:

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

Maar voor Car, wilt u alleen de Model eigenschap doorzoeken:

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

Hoewel u een aangepaste functie voor en een andere kunt schrijven voor IQueryable<Person>IQueryable<Car>, voegt de volgende functie deze filtering toe aan een bestaande query, ongeacht het specifieke elementtype.

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

Omdat de TextFilter functie een IQueryable<T> (en niet alleen een IQueryable) gebruikt en retourneert, kunt u nog meer gecompileerde query-elementen toevoegen na het tekstfilter.

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

Methodeaanroepknooppunten toevoegen aan de expressiestructuur van IQueryable<TDelegate>

Als u een in plaats van een IQueryableIQueryable<T>hebt, kunt u de algemene LINQ-methoden niet rechtstreeks aanroepen. Een alternatief is het bouwen van de binnenste expressiestructuur, zoals wordt weergegeven in het vorige voorbeeld, en weerspiegeling gebruiken om de juiste LINQ-methode aan te roepen terwijl de expressiestructuur wordt doorgegeven.

U kunt ook de functionaliteit van de LINQ-methode dupliceren door de hele structuur te verpakken in een structuur die een MethodCallExpression aanroep naar de LINQ-methode vertegenwoordigt:

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

In dit geval hebt u geen tijdelijke aanduiding voor compilatietijd T , dus u gebruikt de Lambda overbelasting waarvoor geen informatie over het type compileertijd is vereist en die een in plaats van een LambdaExpressionExpression<TDelegate>.

De dynamische LINQ-bibliotheek

Het bouwen van expressiestructuren met behulp van fabrieksmethoden is relatief complex; het is eenvoudiger om tekenreeksen op te stellen. De dynamische LINQ-bibliotheek bevat een set extensiemethoden die IQueryable overeenkomen met de standaard LINQ-methoden op Queryableen die tekenreeksen in een speciale syntaxis accepteren in plaats van expressiestructuren. De bibliotheek genereert de juiste expressiestructuur van de tekenreeks en kan de resulterende vertaalde IQueryablestructuur retourneren.

Het vorige voorbeeld kan bijvoorbeeld als volgt worden herschreven:

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