C# LINQ-query's schrijven om query's uit te voeren op gegevens
De meeste query's in de inleidende LINQ-documentatie (Language Integrated Query) worden geschreven met behulp van de declaratieve querysyntaxis van LINQ. De C#-compiler vertaalt de querysyntaxis in methode-aanroepen. Met deze methode-aanroepen worden de standaardqueryoperators geïmplementeerd en hebben namen zoals Where
, Select
, GroupBy
, Join
, Max
en Average
. U kunt ze rechtstreeks aanroepen met behulp van de syntaxis van de methode in plaats van de querysyntaxis.
Querysyntaxis en methodesyntaxis zijn semantisch identiek, maar querysyntaxis is vaak eenvoudiger en gemakkelijker te lezen. Sommige query's moeten worden uitgedrukt als methode-aanroepen. U moet bijvoorbeeld een methodeaanroep gebruiken om een query uit te drukken waarmee het aantal elementen wordt opgehaald dat overeenkomt met een opgegeven voorwaarde. U moet ook een methodeoproep gebruiken voor een query waarmee het element wordt opgehaald dat de maximumwaarde in een bronreeks heeft. In de referentiedocumentatie voor de standaardqueryoperators in de naamruimte wordt doorgaans de syntaxis van de System.Linq methode gebruikt. U moet bekend raken met het gebruik van methodesyntaxis in query's en in query-expressies zelf.
Extensiemethoden voor standaardqueryoperator
In het volgende voorbeeld ziet u een eenvoudige query-expressie en de semantisch equivalente query die is geschreven als een op methode gebaseerde query.
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 + " ");
}
De uitvoer van de twee voorbeelden is identiek. Het type van de queryvariabele is hetzelfde in beide formulieren: IEnumerable<T>.
Aan de rechterkant van de expressie ziet u dat de where
component nu wordt uitgedrukt als een instantiemethode voor het numbers
object, dat een type IEnumerable<int>
heeft. Als u bekend bent met de algemene IEnumerable<T> interface, weet u dat deze geen methode heeft Where
. Als u echter de IntelliSense-voltooiingslijst aanroept in de Visual Studio IDE, ziet u niet alleen een Where
methode, maar ook veel andere methoden, zoals Select
, SelectMany
, Join
en Orderby
. Met deze methoden worden de standaardqueryoperators geïmplementeerd.
Hoewel het lijkt alsof IEnumerable<T> het meer methoden bevat, is dat niet zo. De standaardqueryoperators worden geïmplementeerd als uitbreidingsmethoden. Uitbreidingsmethoden 'uitbreiden' van een bestaand type; ze kunnen worden aangeroepen alsof ze exemplaarmethoden voor het type zijn. De standaardqueryoperators breiden uit IEnumerable<T> en daarom kunt u schrijven numbers.Where(...)
. U brengt extensies binnen het bereik met using
instructies voordat u ze aanroept.
Zie Extensiemethoden voor meer informatie over extensiemethoden. Zie Overzicht van Standard-queryoperators (C#) voor meer informatie over standaardqueryoperators. Sommige LINQ-providers, zoals Entity Framework en LINQ naar XML, implementeren hun eigen standaardqueryoperators en extensiemethoden voor andere typen naast IEnumerable<T>.
Lambda-expressies
In het voorgaande voorbeeld wordt de voorwaardelijke expressie (num % 2 == 0
) doorgegeven als een inlineargument aan de methode Enumerable.Where: Where(num => num % 2 == 0).
Deze inline-expressie is een lambda-expressie. Het is een handige manier om code te schrijven die anders in omslachtigere vorm moet worden geschreven. De num
linkerkant van de operator is de invoervariabele, die overeenkomt met num
in de query-expressie. De compiler kan het type num
afleiden omdat dit numbers
een algemeen IEnumerable<T> type is. De hoofdtekst van de lambda is hetzelfde als de expressie in de querysyntaxis of in een andere C#-expressie of -instructie. Dit kan methode-aanroepen en andere complexe logica bevatten. De retourwaarde is het expressieresultaat. Bepaalde query's kunnen alleen worden uitgedrukt in de syntaxis van de methode en sommige van deze query's vereisen lambda-expressies. Lambda-expressies zijn een krachtig en flexibel hulpmiddel in uw LINQ-werkset.
Composabiliteit van query's
In het voorgaande codevoorbeeld wordt de methode Enumerable.OrderBy aangeroepen met behulp van de puntoperator bij de oproep van Where
.
Where
produceert een gefilterde reeks en sorteert vervolgens Orderby
de reeks die wordt geproduceerd door Where
. Omdat query's een IEnumerable
resultaat geven, stelt u deze samen in de syntaxis van de methode door de methodeaanroepen aan elkaar te koppelen. De compiler voert deze samenstelling uit wanneer u query's schrijft met behulp van querysyntaxis. Omdat de resultaten van de query niet worden opgeslagen in een queryvariabele, kunt u deze op elk gewenst moment wijzigen of gebruiken als basis voor een nieuwe query, zelfs nadat u deze hebt uitgevoerd.
In de volgende voorbeelden ziet u enkele eenvoudige LINQ-query's met behulp van elke eerder vermelde benadering.
Notitie
Deze query's werken op in-memory verzamelingen; de syntaxis is echter identiek aan de syntaxis die wordt gebruikt in LINQ voor entiteiten en LINQ voor XML.
Voorbeeld: querysyntaxis
U schrijft de meeste query's met querysyntaxis om query-expressies te maken. In het volgende voorbeeld ziet u drie query-expressies. De eerste queryexpressie laat zien hoe u resultaten filtert of beperkt door voorwaarden toe te passen met een where
component. Hiermee worden alle elementen in de bronreeks geretourneerd waarvan de waarden groter zijn dan 7 of kleiner dan 3. De tweede expressie laat zien hoe u de geretourneerde resultaten kunt ordenen. De derde expressie laat zien hoe u resultaten groepeert op basis van een sleutel. Deze query retourneert twee groepen op basis van de eerste letter van het woord.
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];
Het type van de query's is IEnumerable<T>. Al deze query's kunnen worden geschreven met behulp var
van het volgende voorbeeld:
var query = from num in numbers...
In elk vorig voorbeeld worden de query's pas uitgevoerd nadat u de queryvariabele in een foreach
instructie of een andere instructie hebt herhaald.
Voorbeeld : syntaxis van methode
Sommige querybewerkingen moeten worden uitgedrukt als een methode-aanroep. De meest voorkomende dergelijke methoden zijn methoden die singleton numerieke waarden retourneren, zoals Sum, Max, Min, , Average, enzovoort. Deze methoden moeten altijd als laatste worden aangeroepen in een query omdat ze één waarde retourneren en niet als bron kunnen fungeren voor een extra querybewerking. In het volgende voorbeeld ziet u een methode-aanroep in een query-expressie:
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);
Als de methode of System.Action parameters heeftSystem.Func<TResult>, worden deze argumenten opgegeven in de vorm van een lambda-expressie, zoals wordt weergegeven in het volgende voorbeeld:
// Query #6.
IEnumerable<int> largeNumbersQuery = numbers2.Where(c => c > 15);
In de vorige query's wordt alleen Query 4 onmiddellijk uitgevoerd, omdat er één waarde wordt geretourneerd en niet een algemene IEnumerable<T> verzameling. De methode zelf gebruikt foreach
of vergelijkbare code om de waarde ervan te berekenen.
Elk van de vorige query's kan worden geschreven met behulp van impliciet typen met var
, zoals wordt weergegeven in het volgende voorbeeld:
// var is used for convenience in these queries
double average = numbers1.Average();
var concatenationQuery = numbers1.Concat(numbers2);
var largeNumbersQuery = numbers2.Where(c => c > 15);
Voorbeeld: syntaxis van gemengde query's en methoden
In dit voorbeeld ziet u hoe u de syntaxis van de methode gebruikt voor de resultaten van een querycomponent. Plaats de query-expressie tussen haakjes en pas de puntoperator toe en roep de methode aan. In het volgende voorbeeld retourneert query 7 een telling van de getallen waarvan de waarde tussen 3 en 7 ligt.
// 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();
Omdat Query #7 één waarde retourneert en geen verzameling, wordt de query onmiddellijk uitgevoerd.
De vorige query kan als volgt worden geschreven met behulp van impliciet typen met var
:
var numCount = (from num in numbers...
Deze kan als volgt worden geschreven in de syntaxis van de methode:
var numCount = numbers.Count(n => n is > 3 and < 7);
Deze kan als volgt worden geschreven door expliciet te typen:
int numCount = numbers.Count(n => n is > 3 and < 7);
Predicaatfilters dynamisch opgeven tijdens runtime
In sommige gevallen weet u pas hoeveel predicaten u moet toepassen op bronelementen in de where
component. Een manier om dynamisch meerdere predicaatfilters op te geven, is door de Contains methode te gebruiken, zoals wordt weergegeven in het volgende voorbeeld. De query retourneert verschillende resultaten op basis van de waarde van id
wanneer de query wordt uitgevoerd.
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
*/
Notitie
In dit voorbeeld worden de volgende gegevensbron en gegevens gebruikt:
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)])
];
U kunt controlestroominstructies, zoals if... else
of switch
, gebruiken om te selecteren tussen vooraf bepaalde alternatieve query's. In het volgende voorbeeld studentQuery
wordt een andere where
component gebruikt als de runtimewaarde oddYear
is true
of 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
*/
Null-waarden verwerken in query-expressies
In dit voorbeeld ziet u hoe u mogelijke null-waarden in bronverzamelingen kunt verwerken. Een objectverzameling zoals een IEnumerable<T> object kan elementen bevatten waarvan de waarde null is. Als een bronverzameling een element is null
of bevat waarvan de waarde is null
en uw query geen waarden verwerkt null
, wordt er een NullReferenceException gegenereerd wanneer u de query uitvoert.
In het volgende voorbeeld worden deze typen en statische gegevensmatrices gebruikt:
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)
];
U kunt een defensieve code gebruiken om een null-verwijzingsonderzondering te voorkomen, zoals wordt weergegeven in het volgende voorbeeld:
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
};
In het vorige voorbeeld filtert de where
component alle null-elementen in de categorieënreeks. Deze techniek is onafhankelijk van de null-controle in de join-component. De voorwaardelijke expressie met null in dit voorbeeld werkt omdat Products.CategoryID
het van het type int?
is, wat afkorting is voor Nullable<int>
.
Als in een join-component slechts één van de vergelijkingssleutels een null-waardetype is, kunt u de andere instellen op een null-waardetype in de query-expressie. In het volgende voorbeeld wordt ervan uitgegaan dat dit EmployeeID
een kolom is die waarden van het type int?
bevat:
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 };
In elk van de voorbeelden wordt het trefwoord van de equals
query gebruikt. U kunt ook patroonkoppeling gebruiken, waaronder patronen voor is null
en is not null
. Deze patronen worden niet aanbevolen in LINQ-query's omdat queryproviders de nieuwe C#-syntaxis mogelijk niet correct interpreteren. Een queryprovider is een bibliotheek waarmee C#-queryexpressies worden omgezet in een systeemeigen gegevensindeling, zoals Entity Framework Core. Queryproviders implementeren de System.Linq.IQueryProvider interface om gegevensbronnen te maken die de System.Linq.IQueryable<T> interface implementeren.
Uitzonderingen in query-expressies verwerken
Het is mogelijk om een methode aan te roepen in de context van een query-expressie. Roep geen methode aan in een query-expressie die een neveneffect kan maken, zoals het wijzigen van de inhoud van de gegevensbron of het genereren van een uitzondering. In dit voorbeeld ziet u hoe u uitzonderingen kunt voorkomen wanneer u methoden aanroept in een query-expressie zonder de algemene .NET-richtlijnen voor het afhandelen van uitzonderingen te schenden. Deze richtlijnen geven aan dat het acceptabel is om een specifieke uitzondering te ondervangen wanneer u begrijpt waarom deze in een bepaalde context is gegenereerd. Zie Aanbevolen procedures voor uitzonderingen voor meer informatie.
In het laatste voorbeeld ziet u hoe u deze gevallen kunt afhandelen wanneer u een uitzondering moet genereren tijdens het uitvoeren van een query.
In het volgende voorbeeld ziet u hoe u uitzonderingsafhandelingscode buiten een query-expressie verplaatst. Deze herstructurering is alleen mogelijk wanneer de methode niet afhankelijk is van variabelen die lokaal zijn voor de query. Het is eenvoudiger om uitzonderingen buiten de query-expressie af te handelen.
// 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());
}
}
In het catch (InvalidOperationException)
blokkeringsblok in het voorgaande voorbeeld verwerkt u de uitzondering op de manier die geschikt is voor uw toepassing.
In sommige gevallen is het beste antwoord op een uitzondering die vanuit een query wordt gegenereerd, mogelijk om de uitvoering van de query onmiddellijk te stoppen. In het volgende voorbeeld ziet u hoe u uitzonderingen verwerkt die kunnen worden gegenereerd vanuit een querytekst. Stel dat dit SomeMethodThatMightThrow
een uitzondering kan veroorzaken waarvoor de uitvoering van de query moet worden gestopt.
Het try
blok plaatst de foreach
lus en niet de query zelf. De foreach
lus is het punt waarop de query wordt uitgevoerd. Runtime-uitzonderingen worden gegenereerd wanneer de query wordt uitgevoerd. Daarom moeten ze in de foreach
lus worden verwerkt.
// 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.
*/
Vergeet niet om elke uitzondering te ondervangen die u verwacht te genereren en/of om eventueel benodigde opschoning in een finally
blok uit te voeren.