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 querysyntaxis moet echter worden omgezet in methodeoproepen voor de .NET Common Language Runtime (CLR) wanneer de code wordt gecompileerd. Met deze methode worden de standaardqueryoperators aangeroepen die namen hebben zoals Where, Select, JoinGroupBy, , Maxen 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. U kunt zien dat het type van de queryvariabele hetzelfde is in beide formulieren: IEnumerable<T>.

Laten we de query op basis van methoden nader bekijken. 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, Joinen Orderby. Met deze methoden worden de standaardqueryoperators geïmplementeerd.

Schermopname van alle standaardqueryoperators in Intellisense.

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(...).

Als u uitbreidingsmethoden wilt gebruiken, brengt u ze binnen het bereik met using instructies. Vanuit het oogpunt van uw toepassing zijn een extensiemethode en een reguliere instantiemethode hetzelfde.

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 vorige voorbeeld ziet u dat de voorwaardelijke expressie (num % 2 == 0) wordt doorgegeven als een inlineargument aan de Enumerable.Where methode: 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 alleen het resultaat van de expressie. Bepaalde query's kunnen alleen worden uitgedrukt in de syntaxis van de methode en sommige query's vereisen lambda-expressies. Lambda-expressies zijn een krachtig en flexibel hulpmiddel in uw LINQ-werkset.

Composabiliteit van query's

In het vorige codevoorbeeld wordt de Enumerable.OrderBy methode aangeroepen met behulp van de puntoperator van de aanroep naar Where. Where produceert een gefilterde reeks en sorteert vervolgens Orderby de reeks die wordt geproduceerd door Where. Omdat query's een IEnumerableresultaat 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 eenvoudige verzamelingen in het geheugen; De basissyntaxis 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 kunnen fungeren als de bron 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.Func<TResult> parameters heeftSystem.Action, 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 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. Over het algemeen is het echter beter om een tweede variabele te gebruiken om het resultaat van de methode-aanroep op te slaan. Op deze manier wordt de query minder vaak verward met de resultaten van de query.

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

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 nullen uw query geen waarden verwerkt null , wordt er een NullReferenceException gegenereerd wanneer u de query uitvoert.

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

Zie ook