Écriture de requêtes LINQ C# pour interroger des données

La plupart des requêtes de la documentation de présentation de Langage Integrated Query (LINQ) sont écrites à l’aide de la syntaxe de requête déclarative de LINQ. Toutefois, la syntaxe de requête doit être traduite en appels de méthode pour le Common Langage Runtime (CLR) .NET lorsque le code est compilé. Ces appels de méthode appellent les opérateurs de requête standard, qui ont des noms tels que Where, Select, GroupBy, Join, Max et Average. Vous pouvez les appeler directement en utilisant la syntaxe de méthode à la place de la syntaxe de requête.

La syntaxe de requête et la syntaxe de méthode sont identiques sémantiquement, mais la syntaxe de requête est souvent plus simple et plus facile à lire. Certaines requêtes doivent être exprimées en tant qu’appels de méthode. Par exemple, vous devez utiliser un appel de méthode pour exprimer une requête qui récupère le nombre d’éléments qui correspondent à une condition spécifiée. Vous devez également utiliser un appel de méthode pour une requête qui récupère dans une séquence source l’élément qui a la valeur maximale. En général, la documentation de référence des opérateurs de requête standard dans l’espace de noms System.Linq utilise la syntaxe de méthode. Vous devez vous familiariser avec l’utilisation de la syntaxe de méthode dans des requêtes et dans des expressions de requête elles-mêmes.

Méthodes d’extension d’opérateur de requête standard

L’exemple suivant présente une expression de requête simple et la requête sémantiquement équivalente écrite en tant que requête fondée sur une méthode.

int[] numbers = [ 5, 10, 8, 3, 6, 12 ];

//Query syntax:
IEnumerable<int> numQuery1 =
    from num in numbers
    where num % 2 == 0
    orderby num
    select num;

//Method syntax:
IEnumerable<int> numQuery2 = numbers.Where(num => num % 2 == 0).OrderBy(n => n);

foreach (int i in numQuery1)
{
    Console.Write(i + " ");
}
Console.WriteLine(System.Environment.NewLine);
foreach (int i in numQuery2)
{
    Console.Write(i + " ");
}

La sortie des deux exemples est identique. Vous pouvez voir que le type de la variable de requête est le même dans les deux formes : IEnumerable<T>.

Pour comprendre la requête basée sur une méthode, examinons-la de plus près. Du côté droit de l’expression, remarquez que la clause where est maintenant exprimée comme une méthode d’instance sur l’objet numbers qui a un type IEnumerable<int>. Si vous connaissez bien l’interface générique IEnumerable<T>, vous savez qu’elle n’a pas de méthode Where. Toutefois, si vous appelez la liste de saisie semi-automatique IntelliSense dans l’IDE de Visual Studio, vous ne voyez pas seulement une méthode Where, mais de nombreuses autres méthodes telles que Select, SelectMany, Join et Orderby. Ces méthodes implémentent les opérateurs de requête standard.

Capture d’écran montrant tous les opérateurs de requête standard dans Intellisense.

Bien que IEnumerable<T> semble inclure davantage de méthodes, ce n’est pas le cas. Les opérateurs de requête standard sont implémentés en tant que méthodes d’extension. Les méthodes d’extension « étendent » un type existant. Elles peuvent être appelées comme s’il s’agissait de méthodes d’instance sur le type. Les opérateurs de requête standard étendent IEnumerable<T>, si bien que vous pouvez écrire numbers.Where(...).

Pour utiliser des méthodes d’extension, vous les placez dans l’étendue avec des directives using. Du point de vue de votre application, une méthode d’extension et une méthode d’instance normale sont identiques.

Pour plus d’informations sur les méthodes d’extension, consultez Méthodes d’extension. Pour plus d’informations sur les opérateurs de requête standard, consultez Présentation des opérateurs de requête standard (C#). Certains fournisseurs LINQ, tels que Entity Framework et LINQ to XML, implémentent leurs propres opérateurs de requête standard et méthodes d’extension supplémentaires pour d’autres types que IEnumerable<T>.

Expressions lambda

Dans l’exemple précédent, notez que l’expression conditionnelle (num % 2 == 0) est passée comme argument de ligne à la méthode Enumerable.Where : Where(num => num % 2 == 0). Cette expression inlined est appelée expression lambda. C’est un moyen pratique d’écrire du code qui devrait sinon être écrit sous une forme plus lourde. L’élément num situé à gauche de l’opérateur est la variable d’entrée qui correspond à num dans l’expression de requête. Le compilateur peut déduire le type de num, car il sait que numbers est un type IEnumerable<T> générique. Le corps de l’expression lambda est identique à l’expression dans la syntaxe de requête ou dans toute autre expression ou instruction C#. Il peut inclure des appels de méthode et d’autres logiques complexes. La valeur de retour est simplement le résultat de l’expression. Certaines requêtes peuvent être exprimées uniquement dans une syntaxe de méthode. Parmi elles, certaines nécessitent des expressions lambda. Les expressions lambda constituent un outil puissant et flexible dans votre boîte à outils LINQ.

Composabilité des requêtes

Dans l’exemple de code précédent, notez que la méthode Enumerable.OrderBy est appelée en utilisant l’opérateur point sur l’appel à Where. Where produit une séquence filtrée, puis Orderby trie la séquence produite par Where. Étant donné que les requêtes retournent un IEnumerable, vous les composez dans la syntaxe de méthode en chaînant les appels de méthode ensemble. Le compilateur effectue cette composition lorsque vous écrivez des requêtes en tirant parti de la syntaxe de requête. Étant donné qu’une variable de requête ne stocke pas les résultats de la requête, vous pouvez la modifier ou l’utiliser à tout moment comme base d’une nouvelle requête, même après son exécution.

Les exemples suivants montrent des requêtes LINQ simples en utilisant chaque approche répertoriée précédemment.

Remarque

Ces requêtes fonctionnent sur les collections simples en mémoire ; toutefois, la syntaxe de base est identique à celle utilisée dans LINQ to Entities et LINQ to XML.

Exemple – Syntaxe de requête

Vous écrivez la plupart des requêtes avec une la syntaxe de requête pour créer des expressions de requête. L’exemple suivant présente trois expressions de requête. La première expression de requête montre comment filtrer ou restreindre des résultats en appliquant des conditions avec une clause where. Tous les éléments de la séquence source dont la valeur est supérieure à 7 ou inférieure à 3 sont retournés. La deuxième expression montre comment classer les résultats retournés. La troisième expression montre comment regrouper des résultats en fonction d’une clé. Cette requête retourne deux groupes en fonction de la première lettre du mot.

List<int> numbers = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0];

// The query variables can also be implicitly typed by using var

// Query #1.
IEnumerable<int> filteringQuery =
    from num in numbers
    where num is < 3 or > 7
    select num;

// Query #2.
IEnumerable<int> orderingQuery =
    from num in numbers
    where num is < 3 or > 7
    orderby num ascending
    select num;

// Query #3.
string[] groupingQuery = ["carrots", "cabbage", "broccoli", "beans", "barley"];
IEnumerable<IGrouping<char, string>> queryFoodGroups =
    from item in groupingQuery
    group item by item[0];

Le type des requêtes est IEnumerable<T>. Toutes ces requêtes pourraient être écrites à l’aide de var, comme illustré dans l’exemple suivant :

var query = from num in numbers...

Dans chacun des exemples précédents, les requêtes ne s’exécutent pas réellement tant vous n’avez pas itéré la variable de requête dans une instruction foreach ou une autre instruction.

Exemple – Syntaxe de méthode

Certaines opérations de requête doivent être exprimées comme un appel de méthode. Les plus répandues de ces méthodes retournent des valeurs numériques singleton, telles que Sum, Max, Min, Average et ainsi de suite. Ces méthodes doivent toujours être appelées en dernier dans toutes les requêtes, car elles retournent une valeur unique et ne peuvent pas servir de source pour une opération de requête supplémentaire. L’exemple suivant présente un appel de méthode dans une expression de requête :

List<int> numbers1 = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0];
List<int> numbers2 = [15, 14, 11, 13, 19, 18, 16, 17, 12, 10];

// Query #4.
double average = numbers1.Average();

// Query #5.
IEnumerable<int> concatenationQuery = numbers1.Concat(numbers2);

Si la méthode a des paramètres System.Action ou System.Func<TResult>, ces arguments sont fournis sous la forme d’une expression lambda, comme dans l’exemple suivant :

// Query #6.
IEnumerable<int> largeNumbersQuery = numbers2.Where(c => c > 15);

Dans les requêtes précédentes, seule Requête #4 s’exécute immédiatement car elle retourne une valeur unique, et non pas une collection IEnumerable<T> générique. La méthode elle-même utilise foreach ou du code similaire pour calculer sa valeur.

Chacune des requêtes précédentes peut être écrite en utilisant des types implicites avec `var``, comme dans l’exemple suivant :

// var is used for convenience in these queries
double average = numbers1.Average();
var concatenationQuery = numbers1.Concat(numbers2);
var largeNumbersQuery = numbers2.Where(c => c > 15);

Exemple – Syntaxe de méthode et de requête mixte

Cet exemple montre comment utiliser la syntaxe de méthode sur les résultats d’une clause de requête. Encadrez simplement l’expression de requête entre parenthèses, puis appliquez l’opérateur point et appelez la méthode. Dans l’exemple suivant, la requête 7 retourne les nombres dont la valeur est comprise entre 3 et 7. Cependant, il est généralement préférable d’utiliser une deuxième variable pour stocker le résultat de l’appel de méthode. De cette manière, il est moins probable que la requête ne soit confondue avec les résultats de la requête.

// Query #7.

// Using a query expression with method syntax
var numCount1 = (
    from num in numbers1
    where num is > 3 and < 7
    select num
).Count();

// Better: Create a new variable to store
// the method call result
IEnumerable<int> numbersQuery =
    from num in numbers1
    where num is > 3 and < 7
    select num;

var numCount2 = numbersQuery.Count();

Comme la requête 7 retourne une valeur unique et non une collection, la requête s’exécute immédiatement.

La requête précédente peut être écrite en utilisant des types implicites avec var, comme suit :

var numCount = (from num in numbers...

Elle peut être écrite dans la syntaxe de méthode comme suit :

var numCount = numbers.Count(n => n is > 3 and < 7);

Elle peut être écrite en utilisant des types explicites, comme suit :

int numCount = numbers.Count(n => n is > 3 and < 7);

Spécifier dynamiquement des filtres de prédicat au moment de l’exécution

Dans certains cas, ce n’est qu’au moment de l’exécution que vous savez combien de prédicats vous devez appliquer aux éléments sources dans la clause where. L’une des manières de spécifier plusieurs filtres de prédicat de manière dynamique consiste à utiliser la méthode Contains, comme indiqué dans l’exemple suivant. La requête retourne des résultats différents en fonction de la valeur de id lors de l’exécution de la requête.

int[] ids = [111, 114, 112];

var queryNames =
    from student in students
    where ids.Contains(student.ID)
    select new
    {
        student.LastName,
        student.ID
    };

foreach (var name in queryNames)
{
    Console.WriteLine($"{name.LastName}: {name.ID}");
}

/* Output:
    Garcia: 114
    O'Donnell: 112
    Omelchenko: 111
 */

// Change the ids.
ids = [122, 117, 120, 115];

// The query will now return different results
foreach (var name in queryNames)
{
    Console.WriteLine($"{name.LastName}: {name.ID}");
}

/* Output:
    Adams: 120
    Feng: 117
    Garcia: 115
    Tucker: 122
 */

Vous pouvez utiliser des instructions de flux de contrôle, telles que if... else ou switch, pour effectuer une sélection parmi des requêtes alternatives prédéterminées. Dans l’exemple suivant, studentQuery utilise une clause where différente, si la valeur à l’exécution de oddYear est true ou false.

void FilterByYearType(bool oddYear)
{
    IEnumerable<Student> studentQuery = oddYear
        ? (from student in students
           where student.Year is GradeLevel.FirstYear or GradeLevel.ThirdYear
           select student)
        : (from student in students
           where student.Year is GradeLevel.SecondYear or GradeLevel.FourthYear
           select student);
    var descr = oddYear ? "odd" : "even";
    Console.WriteLine($"The following students are at an {descr} year level:");
    foreach (Student name in studentQuery)
    {
        Console.WriteLine($"{name.LastName}: {name.ID}");
    }
}

FilterByYearType(true);

/* Output:
    The following students are at an odd year level:
    Fakhouri: 116
    Feng: 117
    Garcia: 115
    Mortensen: 113
    Tucker: 119
    Tucker: 122
 */

FilterByYearType(false);

/* Output:
    The following students are at an even year level:
    Adams: 120
    Garcia: 114
    Garcia: 118
    O'Donnell: 112
    Omelchenko: 111
    Zabokritski: 121
 */

Gérer des valeurs Null dans des expressions de requête

Cet exemple montre comment gérer d’éventuelles valeurs Null dans des collections sources. Une collection d’objets telle qu’un IEnumerable<T> peut contenir des éléments dont la valeur est null. Si une collection source est null ou contient un élément dont la valeur est null et que votre requête ne gère pas les valeurs null, une NullReferenceException est levée à l’exécution de la requête.

Vous pouvez écrire du code en prévention pour éviter une exception de référence Null, comme indiqué dans l’exemple suivant :

var query1 =
    from c in categories
    where c != null
    join p in products on c.ID equals p?.CategoryID
    select new
    {
        Category = c.Name,
        Name = p.Name
    };

Dans l’exemple précédent, la clause where exclut tous les éléments Null de la séquence de catégories. Cette technique est indépendante de la vérification de valeur Null de la clause join. Dans cet exemple, l’expression conditionnelle avec une valeur Null fonctionne, car Products.CategoryID est de type int?, qui est le raccourci de Nullable<int>.

Dans une clause join, si seulement l’une des clés de comparaison est un type valeur Nullable, vous pouvez caster l’autre clé en type valeur Nullable dans l’expression de requête. Dans l’exemple suivant, supposons que EmployeeID soit une colonne qui contienne des valeurs de type int? :

var query =
    from o in db.Orders
    join e in db.Employees
        on o.EmployeeID equals (int?)e.EmployeeID
    select new { o.OrderID, e.FirstName };

Dans chacun des exemples, le mot clé de requête equals est utilisé. Vous pouvez également utiliser des critères spéciaux, qui incluent des modèles pour is null et is not null. Ces modèles ne sont pas recommandés dans les requêtes LINQ, car les fournisseurs de requêtes peuvent ne pas interpréter correctement la nouvelle syntaxe C#. Un fournisseur de requêtes est une bibliothèque qui traduit des expressions de requête C# dans un format de données natif, tel qu’Entity Framework Core. Les fournisseurs de requêtes implémentent l’interface System.Linq.IQueryProvider pour créer des sources de données qui implémentent l’interface System.Linq.IQueryable<T>.

Gérer des exceptions dans des expressions de requête

Dans le contexte d’une expression de requête, vous pouvez appeler n’importe quelle méthode. Évitez d’appeler toute méthode dans une expression de requête susceptible de créer un effet secondaire, telle que la modification du contenu de la source de données ou la levée d’une exception. Cet exemple montre comment éviter la levée d’exceptions lorsque vous appelez des méthodes dans une expression de requête, en respectant les directives générales .NET relatives à la gestion des exceptions. Selon ces directives, il est acceptable d’intercepter une exception spécifique si vous comprenez pourquoi elle est levée dans un contexte donné. Pour plus d’informations, consultez les Bonnes pratiques pour les exceptions.

Le dernier exemple montre comment gérer les cas où vous devez lever une exception pendant l’exécution d’une requête.

L’exemple suivant montre comment déplacer du code de gestion des exceptions en dehors d’une expression de requête. Cette refactorisation n’est possible que lorsque la méthode ne dépend pas des variables locales de la requête. Il est plus facile de traiter des exceptions en dehors de l’expression de requête.

// A data source that is very likely to throw an exception!
IEnumerable<int> GetData() => throw new InvalidOperationException();

// DO THIS with a datasource that might
// throw an exception.
IEnumerable<int>? dataSource = null;
try
{
    dataSource = GetData();
}
catch (InvalidOperationException)
{
    Console.WriteLine("Invalid operation");
}

if (dataSource is not null)
{
    // If we get here, it is safe to proceed.
    var query =
        from i in dataSource
        select i * i;

    foreach (var i in query)
    {
        Console.WriteLine(i.ToString());
    }
}

Dans le bloc catch (InvalidOperationException) de l’exemple précédent, gérez (ou ne gérez pas) l’exception de manière appropriée pour votre application.

Dans certains cas, la meilleure réponse à la levée d’une exception à l’intérieur d’une requête consiste à arrêter immédiatement l’exécution de la requête. L’exemple suivant montre comment gérer les exceptions pouvant être levées dans le corps d’une requête. Supposons que SomeMethodThatMightThrow puisse provoquer la levée d’une exception qui nécessite l’arrêt de l’exécution de la requête.

Le bloc try englobe la boucle foreach, pas la requête elle-même. C’est au niveau de la boucle foreach que la requête est exécutée. L’exception à l’exécution est levée lorsque la requête est exécutée. Par conséquent, elle doit être gérée dans la boucle foreach.

// Not very useful as a general purpose method.
string SomeMethodThatMightThrow(string s) =>
    s[4] == 'C' ?
        throw new InvalidOperationException() :
        @"C:\newFolder\" + s;

// Data source.
string[] files = ["fileA.txt", "fileB.txt", "fileC.txt"];

// Demonstration query that throws.
var exceptionDemoQuery =
    from file in files
    let n = SomeMethodThatMightThrow(file)
    select n;

try
{
    foreach (var item in exceptionDemoQuery)
    {
        Console.WriteLine($"Processing {item}");
    }
}
catch (InvalidOperationException e)
{
    Console.WriteLine(e.Message);
}

/* Output:
    Processing C:\newFolder\fileA.txt
    Processing C:\newFolder\fileB.txt
    Operation is not valid due to the current state of the object.
 */

N’oubliez pas d’intercepter toute exception que vous prévoyez de déclencher ou d’effectuer les nettoyages nécessaires dans un bloc finally.

Voir aussi