Gå til indholdet

Videre med LINQ

LINQ (Language Integrated Query) er en kraftfuld og fleksibel del af C#, som giver dig mulighed for at arbejde med data på en intuitiv og effektiv måde. Efter at have fået et grundlæggende kendskab til LINQ, er det nu tid til at dykke lidt dybere ned i nogle mere avancerede aspekter.

Information til undervisere

Dette kapitel er en viderebygning på LINQ kapitlet og forudsætter at læseren har et grundlæggende kendskab til LINQ. Kapitlet kan bruges som en opfølgning på det første kapitel, eller som en del af en længere undervisningsforløb om LINQ.

IQueryable og IEnumerable i C#

I C# er IQueryable og IEnumerable to vigtige interfaces, der anvendes i LINQ for at arbejde med samlinger af data. Du vil høre om dem hele tiden så det er vigtigt at forstå forskellene mellem dem.

Den korte version

Valget mellem IEnumerable og IQueryable afhænger af din applikations behov:

  • Brug IEnumerable, når du arbejder med in-memory samlinger.
  • Brug IQueryable, når du arbejder med komplekse forespørgsler mod en eksempelvis en database, hvor du ønsker at udnytte database serverens kapacitet til at behandle data.

IEnumerable

IEnumerable er et interface, der er en del af System.Collections-namespace. Det bruges til at iterere over en samling af objekter i hukommelsen. Når du bruger IEnumerable, udføres alle operationer, såsom filtrering og sortering, i applikationens hukommelse. Det betyder, at hele datasættet skal være indlæst i hukommelsen, før du kan arbejde med det.

  • Bruges primært med in-memory samlinger som arrays, lister osv.
  • LINQ til objekter anvender IEnumerable til at udføre forespørgsler.
  • Når du anvender LINQ-forespørgsler på IEnumerable, returneres der et nyt IEnumerable objekt, der indeholder resultatet.
  • Hvert kald til en LINQ-metode som Where eller SelectIEnumerable returnerer et nyt IEnumerable objekt, og forespørgslen evalueres “lazy”, hvilket betyder, at den kun evalueres, når du itererer over resultatsættet.

Eksempel på brug af IEnumerable:

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        // Opretter en liste af personer
        List<Person> personer = GetPersonList();

        // Filtrer personer ældre end 30 år
        IEnumerable<Person> filtreredePersoner = personer.Where(p => p.Alder > 30);

        // Udskriver de filtrerede personer
        foreach (var person in filtreredePersoner)
        {
            Console.WriteLine($"{person.Navn} er {person.Alder} år gammel");
        }
    }

    static List<Person> GetPersonList()
    {
        return new List<Person>
        {
            new Person { Navn = "Hans", Alder = 25 },
            new Person { Navn = "Grethe", Alder = 35 },
            new Person { Navn = "Jens", Alder = 42 },
            new Person { Navn = "Lise", Alder = 28 }
        };
    }
}

class Person
{
    public string Navn { get; set; }
    public int Alder { get; set; }
}

Info

Se eventuelt aloisdg/awesome-linq for yderligere ressourcer.

IQueryable

IQueryable er en del af System.Linq-namespace og er designet til at arbejde med datakilder, der ligger uden for applikationen, såsom en database. Når du bruger IQueryable, konstrueres forespørgslen i form af en Expression Tree, som så oversættes til en passende forespørgselsformat for datakilden, for eksempel SQL for en SQL Server database.

  • Bruges med datakilder som databases via ORM-frameworks som Entity Framework.
  • Tillader udførelse af forespørgsler på serveren, hvilket kan være mere effektivt, især for store datasæt.
  • LINQ til Entities (eller lignende teknologier) anvender IQueryable til at bygge og udføre forespørgsler.
  • Når en LINQ-forespørgsel anvendes på IQueryable, konstrueres en Expression Tree, som senere oversættes til den specifikke forespørgselssprog for datakilden.

Eksempel på brug af IQueryable:

// EF DbContext
IQueryable<Person> personer = dbContext.Personer;   // Personer er en database tabel
var filtreredePersoner = personer.Where(p => p.Alder > 30);

Lige nu skal du bare kende forskellen mellem IEnumerable og IQueryable.

Avancerede metoder

LINQ tilbyder en rig syntaks, der kan håndtere komplekse dataforespørgsler. En dybere forståelse af denne syntaks åbner for mere avancerede forespørgsler.

Join

Join bruges i LINQ til at kombinere to sekvenser baseret på nøgleværdier og returnere flade par af matchende elementer.

Her er den grundlæggende syntaks:

var result = outerSequence.Join(
    innerSequence,
    outerKeySelector,
    innerKeySelector,
    resultSelector);
  • outerSequence: Den ydre sekvens, som vi vil sammenkæde.
  • innerSequence: Den indre sekvens, der skal sammenkædes med den ydre.
  • outerKeySelector: En funktion til at udtrække nøglen fra den ydre sekvens.
  • innerKeySelector: En funktion til at udtrække nøglen fra den indre sekvens.
  • resultSelector: En funktion, der definerer hvordan de sammenkædede objekter skal projekteres i resultatet.

Her er et eksempel:

var customers = new[]
{
    new { CustomerId = 1, Name = "John" },
    new { CustomerId = 2, Name = "Jane" }
};

var orders = new[]
{
    new { OrderId = 1, CustomerId = 1, Amount = 100 },
    new { OrderId = 2, CustomerId = 2, Amount = 150 },
    new { OrderId = 3, CustomerId = 1, Amount = 200 }
};

var customerOrders = customers.Join(
    orders,
    customer => customer.CustomerId,
    order => order.CustomerId,
    (customer, order) => new
    {
        CustomerName = customer.Name,
        OrderAmount = order.Amount
    });

foreach (var item in customerOrders)
{
    Console.WriteLine($"Customer: {item.CustomerName}, Order Amount: {item.OrderAmount}");
}

Dette eksempel kombinerer kunder og deres ordrer ved hjælp af CustomerId og viser resultaterne som flade par af kundernavne og ordrebeløb.

GroupJoin

GroupJoin bruges i LINQ til at kombinere to sekvenser baseret på nøgleværdier og returnere grupperede resultater. Den giver mulighed for at lave en gruppering af relaterede elementer fra to datakilder.

Her er den grundlæggende syntaks:

var result = outerSequence.GroupJoin(
    innerSequence,
    outerKeySelector,
    innerKeySelector,
    resultSelector);
  • outerSequence: Den ydre sekvens, som vi vil sammenkæde.
  • innerSequence: Den indre sekvens, der skal sammenkædes med den ydre.
  • outerKeySelector: En funktion til at udtrække nøglen fra den ydre sekvens.
  • innerKeySelector: En funktion til at udtrække nøglen fra den indre sekvens.
  • resultSelector: En funktion, der definerer hvordan de sammenkædede objekter skal projekteres i resultatet.

Her er et eksempel:

var customers = new[]
{
    new { CustomerId = 1, Name = "John" },
    new { CustomerId = 2, Name = "Jane" }
};

var orders = new[]
{
    new { OrderId = 1, CustomerId = 1, Amount = 100 },
    new { OrderId = 2, CustomerId = 2, Amount = 150 },
    new { OrderId = 3, CustomerId = 1, Amount = 200 }
};

var customerOrders = customers.GroupJoin(
    orders,
    customer => customer.CustomerId,
    order => order.CustomerId,
    (customer, customerOrders) => new
    {
        CustomerName = customer.Name,
        Orders = customerOrders
    });

foreach (var customer in customerOrders)
{
    Console.WriteLine($"Customer: {customer.CustomerName}");
    foreach (var order in customer.Orders)
    {
        Console.WriteLine($"  Order Amount: {order.Amount}");
    }
}

Dette eksempel kombinerer kunder og deres ordrer ved hjælp af CustomerId og viser resultaterne i en struktureret form.

SelectMany

SelectMany er en metode i LINQ (Language Integrated Query) i C#, der flader en samling af samlinger ud. Det er ofte brugt, når du har en sekvens af sekvenser, og du vil behandle det som en enkelt sekvens.

Her er et eksempel på, hvordan du kan bruge SelectMany:

List<List<int>> listOfLists = new List<List<int>>
{
    new List<int> { 1, 2, 3 },
    new List<int> { 4, 5, 6 },
    new List<int> { 7, 8, 9 }
};

IEnumerable<int> flattened = listOfLists.SelectMany(subList => subList);

foreach(int number in flattened)
{
    Console.WriteLine(number);
}

I dette eksempel har vi en liste af lister af integers. Vi bruger SelectMany til at flade listen af lister ud til en enkelt sekvens af integers. Outputtet vil være:

1
2
3
4
5
6
7
8
9

Her er et andet eksempel. Forestil dig, at vi har en Faktura klasse, som indeholder en liste af FakturaLinje objekter. Vi vil gerne hente en fladet liste af alle fakturalinjer fra en liste af fakturaer:

using System;
using System.Collections.Generic;
using System.Linq;

namespace App
{
    class Program
    {
        static void Main(string[] args)
        {
            // Opretter en liste af fakturaer
            List<Faktura> fakturaer = new List<Faktura>
            {
                new Faktura
                {
                    FakturaId = 1,
                    FakturaLinjer = new List<FakturaLinje>
                    {
                        new FakturaLinje { Produkt = "Blyant", Antal = 10, PrisPrEnhed = 2 },
                        new FakturaLinje { Produkt = "Kuglepen", Antal = 5, PrisPrEnhed = 5 }
                    }
                },
                new Faktura
                {
                    FakturaId = 2,
                    FakturaLinjer = new List<FakturaLinje>
                    {
                        new FakturaLinje { Produkt = "Notesbog", Antal = 3, PrisPrEnhed = 15 },
                        new FakturaLinje { Produkt = "Viskelæder", Antal = 7, PrisPrEnhed = 3 }
                    }
                }
            };

            // Anvender SelectMany til at flade listen af fakturaer ud til en liste af fakturalinjer
            var alleFakturaLinjer = fakturaer.SelectMany(f => f.FakturaLinjer);

            foreach (var linje in alleFakturaLinjer)
            {
                Console.WriteLine($"Produkt: {linje.Produkt}, Antal: {linje.Antal}, Pris pr. enhed: {linje.PrisPrEnhed}");
            }
        }
    }

    public class Faktura
    {
        public int FakturaId { get; set; }
        public List<FakturaLinje> FakturaLinjer { get; set; }
    }

    public class FakturaLinje
    {
        public string Produkt { get; set; }
        public int Antal { get; set; }
        public decimal PrisPrEnhed { get; set; }
    }
}

I dette eksempel har vi to Faktura objekter, hver med sin egen liste af FakturaLinje objekter. Ved at anvende SelectMany, kombinerer vi disse lister af fakturalinjer til en enkelt liste, hvilket gør det lettere at arbejde med dem i en samlet kontekst. Det er en meget effektiv måde at arbejde med samlinger af samlinger på.

SelectMany er meget nyttigt, når du arbejder med komplekse datastrukturer, hvor du har brug for at flade en samling af samlinger ud til en enkelt sekvens.

Brugerdefinerede LINQ Udvidelser

At oprette en brugerdefineret LINQ-udvidelse er en kraftfuld måde at tilføje specifik funktionalitet til LINQ-forespørgsler, der muligvis ikke er dækket af de indbyggede metoder. En brugerdefineret LINQ-udvidelse implementeres som en extensionmetode.

Info

Læs om Extension metoder inden du fortsætter.

Eksempel: Even og Odd

At tilføje Even og Odd som brugerdefinerede LINQ-udvidelser kan være en god idé, da det giver en intuitiv og lettilgængelig måde at filtrere samlinger af tal baseret på, om de er lige eller ulige.

Først defineres udvidelsesmetoderne Even og Odd i en statisk klasse:

public static class LinqExtensions
{
    public static IEnumerable<int> Even(this IEnumerable<int> source)
    {
        return source.Where(n => n % 2 == 0);
    }

    public static IEnumerable<int> Odd(this IEnumerable<int> source)
    {
        return source.Where(n => n % 2 != 0);
    }
}

I disse metoder anvender vi Where-metoden fra LINQ. For Even-metoden tjekker vi, om et tal n er lige ved at se, om resten er 0, når n divideres med 2. For Odd-metoden gør vi det modsatte.

List<int> tal = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var ligeTal = tal.Even();
var uligeTal = tal.Odd();

Console.WriteLine("Lige tal:");
foreach (var n in ligeTal)
{
    Console.WriteLine(n);
}

Console.WriteLine("\nUlige tal:");
foreach (var n in uligeTal)
{
    Console.WriteLine(n);
}

Outputtet for denne kode vil være en liste af lige tal efterfulgt af en liste af ulige tal fra den oprindelige liste tal:

Lige tal:
2
4
6
8
10

Ulige tal:
1
3
5
7
9

Eksempel: TakeEvery

At tilføje en brugerdefineret LINQ-udvidelse, som TakeEvery, der tillader at tage hvert andet, tredje, fjerde element osv., kan være særligt nyttig, når du arbejder med store datasæt og kun er interesseret i en delmængde af dataene baseret på en bestemt intervalsekvens. Her er et eksempel på, hvordan du kan implementere en sådan udvidelse:

Først definerer vi TakeEvery-metoden i en statisk klasse:

public static class LinqExtensions
{
    public static IEnumerable<T> TakeEvery<T>(this IEnumerable<T> source, int interval)
    {
        if (interval <= 0)
        {
            throw new ArgumentException("Interval skal være større end 0", nameof(interval));
        }

        int index = 0;
        foreach (var item in source)
        {
            if (index++ % interval == 0)
            {
                yield return item;
            }
        }
    }
}

I denne metode bruger vi yield return til at returnere hvert interval‘te element fra den givne source samling. Hvis interval er sat til 2, vil metoden returnere hvert andet element, og hvis det er sat til 3, hvert tredje element, og så videre.

Her er et eksempel på, hvordan du kan bruge TakeEvery-metoden:

List<int> tal = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var hvertAndetTal = tal.TakeEvery(2);
var hvertTredjeTal = tal.TakeEvery(3);

Console.WriteLine("Hvert andet tal:");
foreach (var n in hvertAndetTal)
{
    Console.WriteLine(n);
}

Console.WriteLine("\nHvert tredje tal:");
foreach (var n in hvertTredjeTal)
{
    Console.WriteLine(n);
}

Outputtet for denne kode vil være en liste af hvert andet tal efterfulgt af en liste af hvert tredje tal fra den oprindelige liste tal.

Hvert andet tal:
1
3
5
7
9

Hvert tredje tal:
1
4
7
10

LINQ og performance

LINQ (Language Integrated Query) er en kraftfuld feature i C# og .NET, der tillader udviklere at skrive komplekse data queries direkte i C#. Men som med enhver anden teknologi, kan brugen af LINQ have potentielle performanceproblemer, hvis det ikke bruges korrekt. Her er nogle af de mest almindelige områder, hvor LINQ kan påvirke performance:

  1. Unødvendig gentagelse af queries: LINQ queries er “lazy” i det, de kun udføres, når resultatet er nødvendigt. Dette betyder, at hver gang du refererer til en LINQ query, bliver den udført igen. Hvis du bruger det samme resultat flere gange, skal du gemme resultatet i en variabel og genbruge den i stedet for at udføre den samme query flere gange. Du kan også bruge .ToList() eller .ToArray() for at tvinge udførelsen af en query med det samme.

  2. For tidlig udførelse af queries: På den anden side kan det at tvinge en query til at blive udført for tidligt (for eksempel ved at bruge .ToList() eller .ToArray()) også være et problem, hvis du kun har brug for en del af dataene. Hvis du kun har brug for det første element, der opfylder en betingelse, kan det være mere effektivt at bruge .FirstOrDefault() i stedet for at udføre hele queryen og derefter hente det første element.

  3. Brug af ineffektive operators: Nogle LINQ operators kan være meget ineffektive, hvis de ikke bruges korrekt. For eksempel, .Contains(), .Any(), .All(), .First(), .Last() kan alle potentielt gennemgå hele samlingen. Hvis du arbejder med store samlinger, skal du være opmærksom på, hvilke operators du bruger.

  4. Brug af LINQ i hot paths: LINQ er generelt langsommere end traditionelle loops, fordi det medfører ekstra overhead for at tilbyde sin fleksible syntaks og features. Hvis du har en del af din kode, der bliver kaldt meget ofte (en “hot path”), kan det være mere effektivt at bruge traditionelle loops i stedet for LINQ.

  5. Brug af LINQ med Entity Framework eller andre ORMs: Når du bruger LINQ med Entity Framework eller andre Object-Relational Mappers (ORMs), skal du være opmærksom på, at dine LINQ queries bliver oversat til SQL. Nogle LINQ queries kan resultere i meget ineffektiv SQL, hvilket kan medføre performanceproblemer. Det er altid en god ide at teste dine LINQ queries og overvåge den genererede SQL for at sikre, at den er effektiv.

Overordnet set, selvom LINQ kan være meget kraftfuldt og bekvemt, er det vigtigt at være opmærksom på, hvordan det kan påvirke performance. Som med enhver anden teknologi, er den bedste måde at sikre god performance at profilere din kode, identificere flaskehalse og

Opgaver

Eksterne biblioteker

Der findes en del 3. parts biblioteker der udvider LINQ - se eksempelvis