Concepts de base des expressions de requête

Cet article présente les concepts fondamentaux liés aux expressions de requête en C#.

Qu’est-ce qu’une requête et quel est son rôle ?

Une requête est un jeu d’instructions qui décrit quelles données doivent être récupérées à partir d’une source (ou de sources) de données fournie, et quelles forme et organisation les données retournées doivent avoir. Une requête est distincte des résultats qu’elle génère.

De manière générale, les données sources sont organisées logiquement comme une séquence d’éléments du même type. Par exemple, une table de base de données SQL contient une séquence de lignes. Un fichier XML contient une « séquence » d’éléments XML (bien que ceux-ci soient organisés hiérarchiquement dans une arborescence). Une collection en mémoire contient une séquence d’objets.

Du point de vue d’une application, le type et la structure spécifiques des données sources d’origine ne sont pas importants. L’application voit toujours les données sources comme une collection IEnumerable<T> ou IQueryable<T>. Par exemple, dans LINQ to XML, les données sources sont rendues visibles sous la forme d’un IEnumerable<XElement>.

Compte tenu de cette séquence source, une requête peut effectuer l’une des trois actions suivantes :

  • Récupérer un sous-ensemble des éléments pour générer une nouvelle séquence sans modifier les éléments individuels. Il est possible que la requête trie alors ou regroupe la séquence retournée de plusieurs façons, comme indiqué dans l’exemple suivant (supposons que scores est un int[]) :

    IEnumerable<int> highScoresQuery =
        from score in scores
        where score > 80
        orderby score descending
        select score;
    
  • Récupérer une séquence d’éléments comme dans l’exemple précédent, mais les transformer en un nouveau type d’objet. Par exemple, une requête peut récupérer uniquement les noms de famille de certains enregistrements clients dans une source de données. Elle peut aussi récupérer l’enregistrement complet et l’utiliser pour construire un autre type d’objet en mémoire, voire des données XML, avant de générer la séquence de résultat final. L’exemple suivant affiche une projection d’un int en string. Notez le nouveau type de highScoresQuery.

    IEnumerable<string> highScoresQuery2 =
        from score in scores
        where score > 80
        orderby score descending
        select $"The score is {score}";
    
  • Récupérer une valeur singleton sur les données sources, par exemple :

    • Nombre d’éléments qui correspondent à une certaine condition.

    • Élément qui a la valeur la plus grande ou la plus petite.

    • Premier élément qui correspond à une condition, ou somme de valeurs particulières dans un jeu d’éléments spécifié. Par exemple, la requête suivante retourne le nombre de notes supérieures à 80 à partir du tableau d’entiers scores :

      var highScoreCount = (
          from score in scores
          where score > 80
          select score
      ).Count();
      

      Dans l’exemple précédent, notez l’utilisation de parenthèses autour de l’expression de requête avant l’appel à la méthode Enumerable.Count. Vous pouvez également utiliser une nouvelle variable pour stocker le résultat concret.

      IEnumerable<int> highScoresQuery3 =
          from score in scores
          where score > 80
          select score;
      
      var scoreCount = highScoresQuery3.Count();
      

Dans l’exemple précédent, la requête est exécutée dans l’appel à Count, car Count doit effectuer une itération sur les résultats afin de déterminer le nombre d’éléments retournés par highScoresQuery.

Qu’est-ce qu’une expression de requête ?

Une expression de requête est une requête exprimée dans la syntaxe de la requête. Une expression de requête est une construction de langage de premier ordre. Elle est semblable à toute autre expression et peut être utilisée dans tous les contextes dans lesquels une expression C# est valide. Une expression de requête se compose d’un jeu de clauses écrit dans une syntaxe déclarative semblable à du SQL ou du XQuery. Chaque clause contient à son tour une ou plusieurs expressions C# et il est possible que ces expressions soient elles-mêmes une expression de requête ou contiennent une expression de requête.

Une expression de requête doit commencer par une clause from et doit se terminer par une clause select ou group. Entre la première clause from et la dernière clause select ou group, elle peut contenir une ou plusieurs de ces clauses facultatives : where, orderby, join, let et même d’autres clauses from. Vous pouvez également utiliser le mot clé into pour que le résultat d’une clause join ou group puisse servir de source pour d’autres clauses de requête dans la même expression de requête.

Variable de requête

Dans LINQ, une variable de requête correspond à n’importe quelle variable qui stocke une requête au lieu des résultats d’une requête. Plus concrètement, une variable de requête est toujours un type énumérable qui produit une séquence d’éléments quand elle est itérée dans une instruction foreach ou un appel direct à sa méthode IEnumerator.MoveNext().

Remarque

Les exemples de cet article utilisent la source de données et les exemples de données suivants.

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

L’exemple de code suivant montre une expression de requête simple avec une source de données, une clause de filtrage, une clause de classement et aucune transformation des éléments sources. La clause select termine la requête.

// Data source.
int[] scores = [90, 71, 82, 93, 75, 82];

// Query Expression.
IEnumerable<int> scoreQuery = //query variable
    from score in scores //required
    where score > 80 // optional
    orderby score descending // optional
    select score; //must end with select or group

// Execute the query to produce the results
foreach (var testScore in scoreQuery)
{
    Console.WriteLine(testScore);
}

// Output: 93 90 82 82

Dans l’exemple précédent, scoreQuery est une variable de requête qui est parfois désignée simplement sous le nom de requête. La variable de requête ne stocke aucune donnée de résultat réelle, générée dans la boucle foreach. En outre, quand l’instruction foreach s’exécute, les résultats de la requête ne sont pas retournés via la variable de requête scoreQuery. Ils sont plutôt retournés via la variable d’itération testScore. La variable scoreQuery peut être itérée dans une deuxième boucle foreach. Elle génère les mêmes résultats tant que ni elle ni la source de données ne sont modifiées.

Une variable de requête peut stocker une requête exprimée dans la syntaxe de requête ou la syntaxe de méthode, ou dans une combinaison des deux. Dans les exemples suivants, queryMajorCities et queryMajorCities2 sont des variables de requête :

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

//Query syntax
IEnumerable<City> queryMajorCities =
    from city in cities
    where city.Population > 100000
    select city;

// Execute the query to produce the results
foreach (City city in queryMajorCities)
{
    Console.WriteLine(city);
}

// Output:
// City { Population = 120000 }
// City { Population = 112000 }
// City { Population = 150340 }

// Method-based syntax
IEnumerable<City> queryMajorCities2 = cities.Where(c => c.Population > 100000);

D’autre part, les deux exemples suivants affichent des variables qui ne sont pas des variables de requête, même si chacune d’entre elles est initialisée avec une requête. Il ne s’agit pas de variables de requête, car elles stockent des résultats :

var highestScore = (
    from score in scores
    select score
).Max();

// or split the expression
IEnumerable<int> scoreQuery =
    from score in scores
    select score;

var highScore = scoreQuery.Max();
// the following returns the same result
highScore = scores.Max();
var largeCitiesList = (
    from country in countries
    from city in country.Cities
    where city.Population > 10000
    select city
).ToList();

// or split the expression
IEnumerable<City> largeCitiesQuery =
    from country in countries
    from city in country.Cities
    where city.Population > 10000
    select city;
var largeCitiesList2 = largeCitiesQuery.ToList();

Types explicites et implicites de variables de requête

Cette documentation fournit généralement le type explicite de la variable de requête pour afficher la relation de type entre la variable de requête et la clause select. Toutefois, vous pouvez également utiliser le mot clé var pour indiquer au compilateur de déduire le type d’une variable de requête (ou toute autre variable locale) au moment de la compilation. Par exemple, l’exemple de requête montré précédemment dans cet article peut également être exprimé en utilisant le type implicite :

var queryCities =
    from city in cities
    where city.Population > 100000
    select city;

Dans l’exemple précédent, l’utilisation de var est facultative. queryCities est une valeur IEnumerable<City> implicitement ou explicitement typée.

Démarrage d’une expression de requête

Une expression de requête doit commencer par une clause from. Elle spécifie une source de données avec une variable de portée. La variable de portée représente chaque élément consécutif dans la séquence source comme la séquence source parcourue. La variable de portée est fortement typée selon le type d’éléments dans la source de données. Dans l’exemple suivant, comme countries est un tableau d’objets Country, la variable de portée est également de type Country. Étant donné que la variable de portée est fortement typée, vous pouvez utiliser l’opérateur point pour accéder à tous les membres disponibles du type.

IEnumerable<Country> countryAreaQuery =
    from country in countries
    where country.Area > 500000 //sq km
    select country;

La variable de portée est dans la portée tant que la requête n’est pas quittée avec un point-virgule ou une clause continuation.

Il est possible qu’une expression de requête contienne plusieurs clauses from. Utilisez des d’autres clauses from quand chaque élément de la séquence source est lui-même une collection ou contient une collection. Par exemple, supposons que vous avez une collection d’objets Country et que chacun d’entre eux contient une collection d’objets City nommés Cities. Pour interroger les objets City dans chaque Country, utilisez deux clauses from comme indiqué ici :

IEnumerable<City> cityQuery =
    from country in countries
    from city in country.Cities
    where city.Population > 10000
    select city;

Pour plus d’informations, consultez from, clause.

Fin d’une expression de requête

Une expression de requête doit se terminer par une clause group ou une clause select.

group, clause

Utilisez la clause group pour générer une séquence de groupes organisée par une clé que vous spécifiez. La clé peut correspondre à tout type de données. Par exemple, la requête suivante crée une séquence de groupes qui contient un ou plusieurs objets Country et dont la clé est un type char avec la valeur étant la première lettre des noms des pays.

var queryCountryGroups =
    from country in countries
    group country by country.Name[0];

Pour plus d’informations sur le regroupement, consultez group, clause.

select, clause

Utilisez la clause select pour générer tous les autres types de séquences. Une clause select simple génère simplement une séquence du même type d’objets que les objets contenus dans la source de données. Dans cet exemple, la source de données contient des objets Country. La clause orderby trie simplement les éléments dans un nouvel ordre et la clause select génère une séquence des objets Country réorganisés.

IEnumerable<Country> sortedQuery =
    from country in countries
    orderby country.Area
    select country;

La clause select peut être utilisée pour transformer des données sources en séquences de nouveaux types. Cette transformation est également appelée projection. Dans l’exemple suivant, la clause selectprojette une séquence de types anonymes qui contient uniquement un sous-ensemble des champs dans l’élément d’origine. Les nouveaux objets sont initialisés en utilisant un initialiseur d’objet.

var queryNameAndPop =
    from country in countries
    select new
    {
        Name = country.Name,
        Pop = country.Population
    };

Dans cet exemple, le var est requis, car la requête produit un type anonyme.

Pour plus d’informations sur toutes les façons d’utiliser une clause select pour transformer des données sources, consultez select, clause.

Continuations avec into

Vous pouvez utiliser le mot clé into dans une clause select ou group pour créer un identificateur temporaire qui stocke une requête. Utilisez la clause into quand vous devez effectuer des opérations de requête supplémentaires sur une requête après une opération de regroupement ou de sélection. Dans l’exemple suivant, les countries sont regroupés en fonction de la population, par plages de 10 millions. Une fois ces groupes créés, d’autres clauses éliminent des groupes par filtrage, puis trient les groupes par ordre croissant. Pour effectuer ces opérations supplémentaires, la continuation représentée par countryGroup est requise.

// percentileQuery is an IEnumerable<IGrouping<int, Country>>
var percentileQuery =
    from country in countries
    let percentile = (int)country.Population / 10_000_000
    group country by percentile into countryGroup
    where countryGroup.Key >= 20
    orderby countryGroup.Key
    select countryGroup;

// grouping is an IGrouping<int, Country>
foreach (var grouping in percentileQuery)
{
    Console.WriteLine(grouping.Key);
    foreach (var country in grouping)
    {
        Console.WriteLine(country.Name + ":" + country.Population);
    }
}

Pour plus d’informations, consultez into.

Filtrage, classement et jointure

Entre la clause from initiale et la clause select ou group finale, toutes les autres clauses (where, join, orderby, from, let) sont facultatives. Il est possible d’utiliser chacune des clauses facultatives zéro fois ou plusieurs fois dans un corps de requête.

where, clause

Utilisez la clause where pour éliminer par filtrage des éléments des données sources selon une ou plusieurs expressions de prédicat. La clause where de l’exemple suivant a un prédicat avec deux conditions.

IEnumerable<City> queryCityPop =
    from city in cities
    where city.Population is < 200000 and > 100000
    select city;

Pour plus d’informations, consultez where, clause.

orderby, clause

Utilisez la clause orderby pour trier les résultats par ordre croissant ou décroissant. Vous pouvez également spécifier des ordres de tri secondaires. L’exemple suivant effectue un tri principal sur les objets country en utilisant la propriété Area. Il effectue ensuite un tri secondaire en utilisant la propriété Population.

IEnumerable<Country> querySortedCountries =
    from country in countries
    orderby country.Area, country.Population descending
    select country;

Le mot clé ascending est facultatif ; il s’agit de l’ordre de tri par défaut si aucun ordre n’est spécifié. Pour plus d’informations, consultez orderby, clause.

join, clause

Utilisez la clause join pour associer et/ou combiner des éléments d’une source de données avec des éléments d’une autre source de données en fonction d’une comparaison d’égalité entre des clés spécifiées dans chaque élément. Dans LINQ, les opérations de jointure sont effectuées sur les séquences des objets dont les éléments sont des types différents. Après avoir joint deux séquences, vous devez utiliser une instruction select ou group pour spécifier l’élément à stocker dans la séquence de sortie. Vous pouvez également utiliser un type anonyme pour combiner des propriétés de chaque jeu d’éléments associés dans un nouveau type pour la séquence de sortie. L’exemple suivant associe des objets prod dont la propriété Category correspond à l’une des catégories dans le tableau de chaînes categories. Les produits dont Category ne correspond pas à une chaîne quelconque dans categories sont éliminés par filtrage. L’instruction select projette un nouveau type dont les propriétés sont extraites de cat et de prod.

var categoryQuery =
    from cat in categories
    join prod in products on cat equals prod.Category
    select new
    {
        Category = cat,
        Name = prod.Name
    };

Vous pouvez également effectuer une jointure groupée en stockant les résultats de l’opération join dans une variable temporaire à l’aide du mot clé into. Pour plus d’informations, consultez join, clause.

let, clause

Utilisez la clause let pour stocker le résultat d’une expression telle qu’un appel de méthode dans une nouvelle variable de portée. Dans l’exemple suivant, la variable de portée firstName stocke le premier élément du tableau de chaînes retourné par Split.

string[] names = ["Svetlana Omelchenko", "Claire O'Donnell", "Sven Mortensen", "Cesar Garcia"];
IEnumerable<string> queryFirstNames =
    from name in names
    let firstName = name.Split(' ')[0]
    select firstName;

foreach (var s in queryFirstNames)
{
    Console.Write(s + " ");
}

//Output: Svetlana Claire Sven Cesar

Pour plus d’informations, consultez let, clause.

Sous-requêtes dans une expression de requête

Une clause de requête peut elle-même contenir une expression de requête qui est parfois connue sous le nom de sous-requête. Chaque sous-requête démarre avec sa propre clause from qui ne pointe pas nécessairement vers la même source de données dans la première clause from. Par exemple, la requête suivante montre une expression de requête utilisée dans l’instruction select pour récupérer les résultats d’une opération de regroupement.

var queryGroupMax =
    from student in students
    group student by student.Year into studentGroup
    select new
    {
        Level = studentGroup.Key,
        HighestScore = (
            from student2 in studentGroup
            select student2.ExamScores.Average()
        ).Max()
    };

Pour plus d’informations, consultez le Guide pratique pour effectuer une sous-requête sur une opération de regroupement.

Voir aussi