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