Partager via


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

La plupart des requêtes de la documentation LINQ (Language Integrated Query) d’introduction utilisent la syntaxe de requête déclarative LINQ. Le compilateur C# traduit la syntaxe de requête en appels de méthode. Ces appels de méthode implémentent les opérateurs de requête standard. Ils ont des noms tels que Where, , SelectGroupBy, Join, Max, et Average. Vous les appelez directement à l’aide de la syntaxe de méthode au lieu 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. Vous devez exprimer certaines requêtes 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. Familiarisez-vous avec l’utilisation de la syntaxe de méthode dans les requêtes et dans les 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. Le type de la variable de requête est le même dans les deux formes : IEnumerable<T>.

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 membres d'extension qui « étendent » un type existant peuvent être appelés comme s’ils étaient des membres du type. Les opérateurs de requête standard s’étendent IEnumerable<T>et c’est pourquoi vous pouvez écrire numbers.Where(...). Vous intégrez les extensions dans la portée à l’aide des directives using avant de les appeler.

Pour plus d’informations sur les membres d’extension, consultez Membres de l’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 membres d’extension pour d’autres types que IEnumerable<T>.

Expressions lambda

Dans l’exemple précédent, l’expression conditionnelle (num % 2 == 0) est passée en tant qu’argument en ligne à la méthode Enumerable.Where : Where(num => num % 2 == 0). cette expression inline est une 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 le résultat de l’expression. Vous ne pouvez exprimer certaines requêtes que dans la syntaxe de méthode, et certaines de ces requêtes 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 illustrent certaines requêtes LINQ de base à l’aide de chaque approche répertoriée précédemment.

Remarque

Ces requêtes fonctionnent sur des collections en mémoire ; Toutefois, la syntaxe est identique à la syntaxe utilisée dans LINQ to Entities et LINQ to XML.

Exemple : syntaxe de requête

Écrivez la plupart des requêtes avec 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 peuvent être écrites à l’aide var de 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

Vous devez exprimer certaines opérations de requête sous forme d'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. Appelez ces méthodes en dernier dans n’importe quelle requête, car elles retournent une valeur unique et ne peuvent pas servir de source pour d’autres opérations de requête. 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>, fournissez ces arguments sous la forme d'une expression lambda, comme illustré dans l'exemple suivant :

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

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

Vous pouvez écrire chacune des requêtes précédentes à l’aide d’une saisie implicite avec var, comme illustré 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 requête et de méthode mixte

Cet exemple montre comment utiliser la syntaxe de méthode sur les résultats d’une clause de requête. Placez 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.

// 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.

Vous pouvez écrire la requête précédente à l’aide de la saisie implicite avec var, comme suit :

var numCount = (from num in numbers...

Vous pouvez l’écrire dans la syntaxe de méthode comme suit :

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

Vous pouvez l’écrire à l’aide de la saisie explicite, 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
 */

Remarque

Cet exemple utilise la source de données et les données suivantes :

record City(string Name, long Population);
record Country(string Name, double Area, long Population, List<City> Cities);
record Product(string Name, string Category);
static readonly City[] cities = [
    new City("Tokyo", 37_833_000),
    new City("Delhi", 30_290_000),
    new City("Shanghai", 27_110_000),
    new City("São Paulo", 22_043_000),
    new City("Mumbai", 20_412_000),
    new City("Beijing", 20_384_000),
    new City("Cairo", 18_772_000),
    new City("Dhaka", 17_598_000),
    new City("Osaka", 19_281_000),
    new City("New York-Newark", 18_604_000),
    new City("Karachi", 16_094_000),
    new City("Chongqing", 15_872_000),
    new City("Istanbul", 15_029_000),
    new City("Buenos Aires", 15_024_000),
    new City("Kolkata", 14_850_000),
    new City("Lagos", 14_368_000),
    new City("Kinshasa", 14_342_000),
    new City("Manila", 13_923_000),
    new City("Rio de Janeiro", 13_374_000),
    new City("Tianjin", 13_215_000)
];

static readonly Country[] countries = [
    new Country ("Vatican City", 0.44, 526, [new City("Vatican City", 826)]),
    new Country ("Monaco", 2.02, 38_000, [new City("Monte Carlo", 38_000)]),
    new Country ("Nauru", 21, 10_900, [new City("Yaren", 1_100)]),
    new Country ("Tuvalu", 26, 11_600, [new City("Funafuti", 6_200)]),
    new Country ("San Marino", 61, 33_900, [new City("San Marino", 4_500)]),
    new Country ("Liechtenstein", 160, 38_000, [new City("Vaduz", 5_200)]),
    new Country ("Marshall Islands", 181, 58_000, [new City("Majuro", 28_000)]),
    new Country ("Saint Kitts & Nevis", 261, 53_000, [new City("Basseterre", 13_000)])
];

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.

L’exemple suivant utilise ces types et tableaux de données statiques :

record Product(string Name, int CategoryID);
record Category(string Name, int ID);
static Category?[] categories =
[
    new ("brass", 1),
    null,
    new ("winds", 2),
    default,
    new ("percussion", 3)
];

static Product?[] products =
[
    new Product("Trumpet", 1),
    new Product("Trombone", 1),
    new Product("French Horn", 1),
    null,
    new Product("Clarinet", 2),
    new Product("Flute", 2),
    null,
    new Product("Cymbal", 3),
    new Product("Drum", 3)
];

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

Vous pouvez appeler n’importe quelle méthode dans le contexte d’une expression de requête. É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. Ces directives indiquent qu'il est acceptable d'intercepter une exception spécifique lorsque 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. Vous ne pouvez refactoriser cette méthode 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 de l’exemple catch (InvalidOperationException) précédent, gérez (ou ne gérez pas) l’exception de la façon appropriée pour votre application.

Dans certains cas, la meilleure réponse à une exception levée à partir d’une requête peut être d’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, gérez-les dans la foreach boucle.

// 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.
 */

Interceptez l’exception que vous prévoyez de déclencher et effectuez tout nettoyage nécessaire dans un finally bloc.

Voir aussi