Requête basée sur l’état à l’exécution

Dans la plupart des requêtes LINQ, la forme générale de la requête est définie dans le code. Vous pouvez filtrer des éléments à l’aide d’une clause where, trier la collection de sorties à l’aide de orderby, regrouper des éléments ou effectuer un calcul. Votre code peut fournir des paramètres pour le filtre, la clé de tri ou d’autres expressions faisant partie de la requête. Toutefois, la forme globale de la requête ne peut pas être modifiée. Dans cet article, vous allez apprendre des techniques pour utiliser l’interface System.Linq.IQueryable<T> et les types qui l’implémentent pour modifier la forme d’une requête à l’exécution.

Vous utilisez ces techniques pour générer des requêtes à l’exécution, lorsqu’une entrée utilisateur ou un état à l’exécution modifie les méthodes de requête à utiliser dans le cadre de la requête. Vous souhaitez modifier la requête en ajoutant, en supprimant ou en modifiant des clauses de la requête.

Remarque

Veillez à ajouter using System.Linq.Expressions; et using static System.Linq.Expressions.Expression; en haut de votre fichier .cs.

Considérez le code qui définit un IQueryable ou un IQueryable<T> par rapport à une source de données :

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

Chaque fois que vous exécutez le code précédent, exactement la même requête est exécutée. Apprenons à modifier la requête l’étend ou la modifie. Un IQueryable a deux composants essentiels :

  • Expression – représentation, indépendante du langage et des sources de données, des composants de la requête actuelle, sous la forme d’une arborescence d’expression.
  • Provider — instance d’un fournisseur LINQ, qui sait comment matérialiser la requête actuelle en une valeur ou un ensemble de valeurs.

Dans le contexte d’interrogation dynamique, le fournisseur reste généralement le même, tandis que l’arborescence de l’expression de la requête diffère d’une requête à l’autre.

Les arborescences de l’expression sont immuables. Si vous souhaitez une autre arborescence de l’expression – par conséquent une autre requête –, vous devez traduire l’arborescence de l’expression existante en une nouvelle. Les sections suivantes décrivent des techniques spécifiques d’interrogation différentes en réponse à l’état à l’exécution :

  • Utiliser l’état à l’exécution à partir de l’arborescence d’expression
  • Appeler d’autres méthodes LINQ
  • Changer d’arborescence d’expression passée dans les méthodes LINQ
  • Construction d’une arborescence de l’expression Expression<TDelegate> avec les méthodes de fabrique sur Expression
  • Ajouter des nœuds d’appel de méthode à l’arborescence d’expression d’une IQueryable
  • Construire des chaînes et utiliser la bibliothèque Dynamic LINQ

Chacune des techniques permet davantage de fonctionnalités, mais induit un coût d’une complexité accrue.

Utiliser l’état à l’exécution à partir de l’arborescence d’expression

Le moyen le plus simple d’interroger de manière dynamique consiste à référencer directement l’état à l’exécution dans la requête par une variable « closed-over », comme length dans l’exemple de code suivant :

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

L’arborescence de l’expression interne (et donc la requête) n’est pas été modifiée. La requête ne retourne des valeurs différentes que parce que la valeur de length a été modifiée.

Appeler d’autres méthodes LINQ

En règle générale, les méthodes LINQ intégrées sur Queryable effectuent deux étapes :

  • Wrapper l’arborescence d’expression actuelle dans une MethodCallExpression représentant l’appel de méthode.
  • Passer l’arborescence d’expression wrappée au fournisseur, soit pour retourner une valeur via la méthode IQueryProvider.Execute du fournisseur, soit pour retourner un objet de requête traduit via la méthode IQueryProvider.CreateQuery.

Vous pouvez remplacer la requête d’origine par le résultat d’une méthode retournant System.Linq.IQueryable<T> pour obtenir une nouvelle requête. Vous pouvez utiliser l’état à l’exécution, comme dans l’exemple suivant :

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

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

Changer d’arborescence d’expression passée dans les méthodes LINQ

Vous pouvez passer des expressions différentes aux méthodes LINQ, en fonction de l’état à l’exécution :

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

Vous allez peut-être vouloir également composer les différentes sous-expressions à l’aide d’une autre bibliothèque telle que celle de LinqKit, 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);

Construire des arborescences d’expression et des requêtes avec des méthodes de fabrique

Dans tous les exemples vus jusqu’ici, vous connaissez le type d’élément au moment de la compilation – string – et par conséquent le type de la requête – IQueryable<string>. Vous devez peut-être ajouter des composants à une requête de n’importe quel type d’élément ou ajouter des composants différents, en fonction du type d’élément. Vous pouvez créer des arborescences d’expression de la base jusqu’en haut en utilisant les méthodes de fabrique sur System.Linq.Expressions.Expression, et ainsi adapter l’expression au moment de l’exécution à un type d’élément spécifique.

Construction d’une Expression<TDelegate>

Lorsque vous construisez une expression à passer à l’une des méthodes LINQ, vous construisez en fait une instance de System.Linq.Expressions.Expression<TDelegate>, TDelegate étant un type délégué comme Func<string, bool>, Action ou un type délégué personnalisé.

System.Linq.Expressions.Expression<TDelegate> hérite de LambdaExpression qui représente une expression lambda complète comme l’exemple suivant :

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

Une LambdaExpression a deux composants :

  1. Une liste de paramètres — (string x) — représentée par la propriété Parameters.
  2. Un corps — x.StartsWith("a") — représenté par la propriété Body.

Les étapes de base de la construction d’une Expression<TDelegate> sont les suivantes :

  1. Définir des objets ParameterExpression pour chacun des paramètres (le cas échéant) dans l’expression lambda, en utilisant la méthode de fabrique Parameter.
    ParameterExpression x = Parameter(typeof(string), "x");
    
  2. Construisez le corps de votre LambdaExpression en utilisant le ParameterExpression défini, ainsi que les méthodes de fabrique sur Expression. Par exemple, une expression représentant x.StartsWith("a") peut être construite comme ceci :
    Expression body = Call(
        x,
        typeof(string).GetMethod("StartsWith", [typeof(string)])!,
        Constant("a")
    );
    
  3. Wrapper les paramètres et le corps dans une Expression<TDelegate> typée au moment de la compilation, en utilisant la surcharge de méthode de fabrique Lambda appropriée :
    Expression<Func<string, bool>> expr = Lambda<Func<string, bool>>(body, x);
    

Les sections suivantes décrivent un scénario dans lequel vous souhaitez peut-être construire un Expression<TDelegate> à transmettre dans une méthode LINQ. Cela fournit un exemple complet de la procédure à suivre à l’aide des méthodes de fabrique.

Construction d’une requête complète au temps d’exécution

Vous souhaitez écrire des requêtes fonctionnant avec plusieurs types d’entités :

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

Pour l’un de ces types d’entités, vous souhaitez filtrer et retourner uniquement les entités qui ont un texte donné dans l’un de leurs champs string. Pour Person, vous souhaitez rechercher dans les propriétés FirstName et LastName :

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

Par contre, pour Car, vous souhaitez rechercher uniquement dans la propriété Model :

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

Bien que vous puissiez écrire une fonction personnalisée pour IQueryable<Person> et une autre pour IQueryable<Car>, la fonction suivante ajoute ce filtrage à n’importe quelle requête existante, quel que soit le type d’élément spécifique.

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

Étant donné que la fonction TextFilter prend et retourne une IQueryable<T> (et pas juste une IQueryable), vous pouvez ajouter d’autres éléments de requête typés au moment de la compilation après le filtre de texte.

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

Ajouter des nœuds d’appel de méthode à l’arborescence d’expression d’IQueryable<TDelegate>

Si vous avez une IQueryable au lieu d’un IQueryable<T>, vous ne pouvez pas appeler directement les méthodes LINQ génériques. Une alternative consiste à créer l’arborescence de l’expression interne (comme illustré dans l’exemple précédent) et à utiliser la réflexion pour appeler la méthode LINQ appropriée lors du passage de l’arborescence de l’expression.

Vous pouvez également dupliquer les fonctionnalités de la méthode LINQ, en wrappant l’arborescence entière dans une MethodCallExpression qui représente un appel à la méthode 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);
}

Dans ce cas, vous n’avez pas d’espace réservé générique T au moment de la compilation, donc vous allez utiliser la surcharge Lambda qui ne nécessite pas d’informations de type au moment de la compilation et qui produit une LambdaExpression au lieu d’une Expression<TDelegate>.

La bibliothèque Dynamic LINQ

La construction des arborescences de l’expression à l’aide de méthodes de fabrique est relativement complexe, il est plus facile de composer des chaînes. La bibliothèque Dynamic LINQ expose un ensemble de méthodes d’extension sur IQueryable correspondant aux méthodes LINQ standard sur Queryable, et qui acceptent les chaînes dans une syntaxe spéciale au lieu d’arborescences d’expression. La bibliothèque génère l’arborescence d’expression appropriée à partir de la chaîne et peut retourner la IQueryable traduite qui en résulte.

Par exemple, l’exemple précédent pourrait être réécrit comme ceci :

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