LINQ

De Banane Atomic
Aller à la navigationAller à la recherche

Assemblage

Csharp.svg
// Ajoutez au projet une référence à l'assemblage System.Core
using System.Linq;

Enumerable

Générer un énumérable

Csharp.svg
var UnACent = Enumerable.Range(1, 100); // 1 2 ... 100
var DixFoisSept = Enumerable.Repeat(7, 10); // 7 7 ... 7

Retourner un énumérable vide

Csharp.svg
return Enumerable.Empty<string>();

LinqPad

Csharp.svg
// afficher le contenu de MyTable
MyTable.Dump();
LinqPad permet d'obtenir l'équivalent SQL d'une requête LINQ.

LINQ to OBJECT

Union – Intersect – Except

Csharp.svg
int[] numbersA = { 0, 1, 2, 3 };
int[] numbersB = { 0, 2, 4, 6 };

// liste des numéros des deux tableaux
IEnumerable<int> allNumbers = numbersA.Concat(numbersB); // 0 1 2 3 0 2 4 6

// liste des numéros des deux tableaux sans les doublons
IEnumerable<int> uniqueNumbers = numbersA.Union(numbersB); // 0 1 2 3 4 6

// liste des numéros présent dans les deux tableaux
IEnumerable<int> duplicateNumbers = numbersA.Intersect(numbersB); // 0 2
// équivalent Comprehension Query Syntax
IEnumerable<int> query = from itemA in numbersA
                         join itemB in numbersB on itemA equals itemB
                         select itemA;

// liste des numéros présent dans numbersA mais pas dans numbersB
IEnumerable<int> exceptNumbers = numbersA.Except(numbersB); // 1 3
Surcharger la comparaison avec IEquatable ou EqualityComparer

Select

Csharp.svg
int[] numbers = { 1, 2, 3 };

var numbersPlusOne = from n in numbers
                     select n + 1;
var numbersPlusOne = numbers.Select(n => n + 1);
// numbersPlusOne : 2 3 4

Async

Csharp.svg
public static Task<T[]> WhenAll<T>(this IEnumerable<Task<T>> source)
{
    return Task.WhenAll(source);
}

var result = await myEnumerable.Select(MyMethodAsync).WhenAll();

public static async Task<IEnumerable<TResult>> SelectAsync<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, Task<TResult>> func)
{
    return await Task.WhenAll(source.Select(func));
}

var result = await myEnumerable.SelectAsync(MyMethodAsync);

SelectMany

Même chose que Select, mais aplatit les résultat en une seule énumération.

Cs.svg
var numbers = new List<List<int>>() {
    new List<int>() { 1, 2, 3 },
    new List<int>() { 4, 5, 6 }
};

// List<List<int>> → IEnumerable<List<int>>
var numbersSelect = numbers.Select(li => li);
// List<ListInt> → IEnumerable<int>, le résultat est aplatit en une seule énumération: [1, 2, 3, 4, 5, 6]
var numbersSelectMany = numbers.SelectMany(li => li);
var numbersSelectMany = from li in numbers
                        from i in li
                        select i;

Async

Csharp.svg
public static Task<T[]> WhenAll<T>(this IEnumerable<Task<T>> source)
{
    return Task.WhenAll(source);
}

var result = (await myEnumerable.Select(MyMethodAsync).WhenAll()).SelectMany(s => s);

public static async Task<IEnumerable<TResult>> SelectManyAsync<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, Task<IEnumerable<TResult>>> func)
{
    return (await Task.WhenAll(source.Select(func))).SelectMany(s => s);
}

var result = await myEnumerable.SelectManyAsync(MyMethodAsync);

OrderBy

Csharp.svg
// trie croissant suivant la longueur des mots
string[] words = { "cherry", "apple", "blueberry" };
var sortedWords = from w in words
                  orderby w.Length
                  select w;
var sortedWords = words.OrderBy(w => w.Length);

// trie décroissant
double[] doubles = { 1.7, 2.3, 1.9, 4.1, 2.9 };
var sortedDoubles = from d in doubles
                    orderby d descending
                    select d;
var sortedDoubles = doubles.OrderByDescending(d => d);

// double trie suivant la taille des mots, 
// et en cas d'égalité suivant l'ordre alphabétique
string[] digits = { "zero", "one", "two", "three", "four", "five", "six",
                    "seven", "eight", "nine" };
var sortedDigits = from d in digits
                   orderby d.Length, d
                   select d;
var sortedDigits = digits.OrderBy(d => d.Length).ThenBy(d => d);

GroupBy

Permet de regrouper suivant une clé. Retourne une liste de IGrouping contenant chacun la valeur de la clé et la liste des éléments correspondant à la clé.
IGrouping<out TKey, out TElement> hérite de IEnumerable<TElement>.

Csharp.svg
// groupement des mots suivant leur première lettre
string[] words = { "blueberry", "chimpanzee", "abacus", "banana", "apple", "cheese" };
var wordGroups = from w in words
                 group w by w[0] into g;
var wordGroups = words.GroupBy(w => w[0]);

foreach (var g in wordGroups)
{
    Console.WriteLine("Words that start with the letter '{0}':", g.Key);
    // g implémente IEnumerable<string>
    foreach (var w in g)
    {
        Console.WriteLine(w);
    }
}
// Result
// Words that start with the letter 'b':
// blueberry
// banana
// Words that start with the letter 'c':
// chimpanzee
// cheese
// Words that start with the letter 'a':
// abacus
// apple

// Variante
var wordGroups = from w in words
                 group w by w[0] into g
                 select new { FirstLetter = g.Key, Words = g };
var wordGroups = words.GroupBy(w => w[0])
                      .Select(g => new { FirstLetter = g.Key, Words = g });

foreach (var g in wordGroups)
{
    Console.WriteLine("Words that start with the letter '{0}':", g.FirstLetter);
    foreach (var w in g.Words)
    {
        Console.WriteLine(w);
    }
}

// Group by sur plusieurs éléments
var groupement= maListe.GroupBy(element => new {element.Propriété1, element.Propriété2});

Avec un delegate nommé

Csharp.svg
// avec un delagate anonyme
var wordGroups = words.GroupBy(w => w[0]);

// avec un delegate nommé
var wordGroups = words.GroupBy(GroupByDelegate);

private char GroupByDelegate(string word)
{
    return word[0];
    // le type de retour est object dans le cas où return renvoie un type anonyme
    // return new { Prop1 = word[0], Prop2 = word[1] };
}

GroupBy + Sum

Cs.svg
var expensesSumByAccountId = expenses.GroupBy(x => x.AccountId)
                                     .ToDictionary(x => x.Key, x => x.Sum(y => y.Amount));

Lookup

Lookup est executé immediatement, à la différence de GroupBy qui est exécuté en différé. Il vaut mieux utiliser Lookup si l'on souhaite parcourir le résultat plus d'une fois.
ILookup<TKey, TElement> hérite de IEnumerable<IGrouping<TKey, TElement>> et se comporte comme un IDictionary<TKey, IEnumerable<TElement>>

Csharp.svg
// groupement des mots suivant leur première lettre
string[] words = { "blueberry", "chimpanzee", "abacus", "banana", "apple", "cheese" };

// GroupBy
IEnumerable<IGrouping<char, string>> wordGroupsGroupBy = words.GroupBy(w => w[0]);
foreach (IGrouping<char, string> g in wordGroupsGroupBy)
{
    Console.WriteLine("Words that start with the letter '{0}':", g.Key);
    foreach (var w in g)
    {
        Console.WriteLine(w);
    }
}

// Lookup
ILookup<char, string> wordGroupsLookup = words.ToLookup(w => w[0]);
foreach (IGrouping<char, string> g in wordGroupsLookup)
{
    Console.WriteLine("Words that start with the letter '{0}':", g.Key);
    foreach (var w in g)
    {
        Console.WriteLine(w);
    }
}
// Même résultats avec GroupBy et Lookup
// Words that start with the letter 'b':
// blueberry
// banana
// Words that start with the letter 'c':
// chimpanzee
// cheese
// Words that start with the letter 'a':
// abacus
// apple

// possibilité d'utiliser ILookup comme un dictionnaire
foreach (string w in wordGroupsLookup['a'])
{
    Console.WriteLine(w);
}
// Resultats
// abacus
// apple

Any – All

Csharp.svg
// Test si au moins un des éléments valide le critère.
string[] words = { "pomme", "fraise", "kiwi", };
bool isWordWithW = words.Any(w => w.Contains("w")); // true

// Test si tous les éléments valident le critère.
int[] numbers = { 1, 11, 3, 19, 41, 65, 19 };
bool onlyOdd = numbers.All(n => n % 2 == 1); // true, tous impair
// attention, si l'énumération est vide, All retourne true
var numbers = Enumerable.Empty<int>();
bool onlyOdd = numbers.All(n => n % 2 == 1);  // true
bool onlyOdd = numbers.DefaultIfEmpty().All(n => n % 2 == 1);  // false
// numbers.DefaultIfEmpty() retourne un enumerable avec un élément de valeur default, ici 0 pour int
// numbers.DefaultIfEmpty(-1) on peut aussi forcer la valeur

// Any permet aussi de tester si une énumération contient des éléments ou non
var numbers = new List<int> { 1, 2 };
bool hasElements = numbers.Any();

Distinct

Spécifier une propriété de distinction

Distinct retourne un IEnumerable<T> sans doublons. La notion d'identique est vérifiée via Equals et GetHashCode.
Cette notion d'identique peut être étendue à une comparaison sur une propriété de l'objet (ex : objet.Nom).

Csharp.svg
Personne[] personnes = {
    new Personne() { Nom = "Nicolas" },
    new Personne() { Nom = "Audrey" },
    new Personne() { Nom = "Nicolas" }
};

personnes.Distinct() // Nicolas Audrey Nicolas
personnes.DistinctBy(personne => personne.Nom) // Nicolas Audrey

Pour cela il faut ajouter une méthode d'extension à IEnumerable<T> :

Csharp.svg
using System.Linq;

public static class IEnumerableExtensions
{
    public static IEnumerable<T> DistinctBy<T>(this IEnumerable<T> source, Func<T, object> uniqueCheckerMethod)
    {
        return source.Distinct(new GenericComparer<T>(uniqueCheckerMethod));
    }
}

class GenericComparer<T> : IEqualityComparer<T>
{
    private Func<T, object> _uniqueCheckerMethod;

    public GenericComparer(Func<T, object> uniqueCheckerMethod)
    {
        this._uniqueCheckerMethod = uniqueCheckerMethod;
    }

    bool IEqualityComparer<T>.Equals(T x, T y)
    {
        return this._uniqueCheckerMethod(x).Equals(this._uniqueCheckerMethod(y));
    }

    int IEqualityComparer<T>.GetHashCode(T obj)
    {
        return this._uniqueCheckerMethod(obj).GetHashCode();
    }
}

Aggregate

Csharp.svg
string sentence = "un deux trois quatre cinq";
string[] words = sentence.Split(' ');

string reversed = words.Aggregate((workingSentence, next) => next + " " + workingSentence);
// premier passage workingSentence = un, next = deux, retour "deux un"
// deuxième passage workingSentence = "deux un", next = trois, retour "trois deux un"

Take / Skip

Csharp.svg
int[] chiffres = { 1, 2, 3, 4 };

// saute les 2 premiers éléments et retourne le reste
chiffres.Skip(2); // 3 4

// prend seulement les 2 premiers éléments
chiffres.Take(2); // 1 2

IEqualityComparer - IEquatable

  1. si la classe implémente IEquatable<T>, Equals(T) est utilisé
  2. sinon les surcharges de Equals et GetHashCode de l'objet sont utilisés
  3. sinon il est possible de spécifier un EqualityComparer<T>
Générer l'EqualityComparer avec ReSharper
Cs.svg
var products1 = new Product[] { new Product { Name = "apple" }, new Product { Name = "orange" } };
var products2 = new Product[] { new Product { Name = "apple" }, new Product { Name = "lemon" } };

products1.Intersect(products2); // empty
// leur référence est utilisé pour les comparer, ils sont donc tous différents même s'ils ont le même Name

product1.Intersect(product2, new ProductEqualityComparer()); // apple
Cs.svg
public class Product : IEquatable<Product>
{
    public string Name { get; set; }

    public bool Equals(Product other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return string.Equals(Name, other.Name);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((Product) obj);
    }

    public override int GetHashCode()
    {
        return (Name != null ? Name.GetHashCode() : 0);
    }
}
Cs.svg
// GetHash code est appelé en premier
// Si les hash codes sont égaux alors Equals est appelé
public class ProductEqualityComparer : EqualityComparer<Product>
{
    public override bool Equals(Product p1, Product p2)
    {
        if (ReferenceEquals(null, p1) || ReferenceEquals(null, p2))
        {
            return false;
        }

        if (ReferenceEquals(p1, p2))
        {
            return true;
        }

        return Equals(p1.Name, p2.Name); // && Equals(p1.Prop2, p2.Prop2);
    }

    public override int GetHashCode(Product p)
    {
        if (p == null)
        {
            return 0;
        }

        // Get hash code for the Name field if it is not null. 
        return (p.Name != null ? p.Name.GetHashCode() : 0);

        // Calcul du hash code avec plusieurs propriétés
        return p.Prop1.GetHashCode() ^ p.Prop2.GetHashCode();

        // Calcul du hash code avec plusieurs propriétés
        unchecked
        {
            var hashCode = (p.Prop1 != null ? p.Prop1.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ (p.Prop2 != null ? p.Prop2.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ (p.Prop3 != null ? p.Prop3.GetHashCode() : 0);
            return hashCode;
        }
    }
}

Zip

Applique une fonction aux éléments de deux séquences pour produire une séquence de résultats.

Cs.svg
int[] numbers = { 1, 2, 3, 4 };
string[] words = { "one", "two", "three" };

IEnumerable<string> numbersAndWords = numbers.Zip(words, (first, second) => first + " " + second);
// 1 one
// 2 two
// 3 three

LINQ to DataSet

Csharp.svg
DataTable clients = dataSet.Tables["client"];
DataTable commandes = dataSet.Tables["commande"];

var query =
    from client in clients.AsEnumerable()
    join commande in commandes.AsEnumerable()
    on client.Field<int>("id") equals commande.Field<int>("clientId")
    where client.Field<string>("name").EndsWith("2")
      && commande.Field<string>("details").EndsWith("2")
    select new
    {
        ClientId = client.Field<int>("id"),
        ClientName = client.Field<string>("name"),
        CommandeId = commande.Field<int>("id"),
        CommandeDetails = commande.Field<string>("details")
    };

Affichage de la requête dans WPF

Xaml.svg
<DataGrid ItemsSource="{Binding Result}" />
Csharp.svg
private IEnumerable _result;
public IEnumerable Result
{
    get
    {
        return _result;
    }
    set
    {
        _result = value;
        OnPropertyChanged("Result");
    }
}

/* ... */

Result = query;

LINQ to SQL

  1. View → Server Explorer → Data Connections → Add Connection
  2. Add → Class → Data → LINQ to SQL Classes (fichier *.dbml)
  3. Glisser les tables dans le designer
Csharp.svg
var db = new DataClasses1DataContext();

var query =
    from client in db.clients
    select new
    {
        ClientId = client.id,
        ClientName = client.name
    };

// affichage du résultat dans un dataGridView WinForm
dataGridView1.DataSource = query;

IQueryable

Étend IEnumerable, et possède

  • Provider: query provider associé au type de données (LINQ to SQL)
  • Expression: arbre d'expression
  • ElementType: type d'élément retourné

Group By

Csharp.svg
// groupement des clients par ville
var query =
    from client in db.clients
    group by client.city into group
    select new
    {
        City = group.Key,
        ClientNumber = group.Sum(c => c.city)
    };

PredicateBuilder

Cs.svg
Expression<Func<Item, bool>> predicate = x => x.Date.Year > 2019;

// concat predicates
var predicate = PredicateBuilder.True<Item>();
predicate = predicate.And(x => x.Date.Year > 2019);
predicate = predicate.And(x => x.Date.Year < 2021);
PredicateBuilder.cs
public static class PredicateBuilder
{
    public static Expression<Func<T, bool>> True<T>() => f => true;
    public static Expression<Func<T, bool>> False<T>() => f => false;

    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
    {
        var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
        return Expression.Lambda<Func<T, bool>>(Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters);
    }

    public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
    {
        var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
        return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters);
    }
}

LINQ to XML

XDocument est disponible à partir du Framework 3.5

Lecture

Ne pas fait directement les requêtes sur XDocument car c'est un nœud virtuel qui ne contient que le nœud racine. Utiliser donc XDocument.Root.

Csharp.svg
// chargement du XML depuis un fichier
XDocument xDoc = XDocument.Load(xmlFilePath);
// ou depuis une chaîne de caratères
XDocument xDoc = XDocument.Parse("<?xml version=\"1.0\"?><root><noeud /></root>");

// Séléctionne tous les noeuds "noeud2" fils direct de "noeud1" 
// qui lui-même est fils direct du noeud racine.
var query = xDoc.Root.Element("noeud1").Elements("noeud2");
var query = from n in xDoc.Root.Element("noeud1").Elements("noeud2")
            select n;

// Séléctionne tous les noeuds "noeud2" fils direct ou non de la racine
var query = xDoc.Root.Descendants("noeud2");

// Récupère le contenu de noeud1, <noeud1>true</noeud1> ici "true" au format string
string content = xDoc.Root.Element("noeud1").Value;
// Convertion
bool b = XmlConvert.ToBoolean(content);

foreach (var noeud in query) // noeud est un XElement
{
    string attribut = noeud.Attribute("attribut").Value;
    string attribut = (string)noeud.Attribute("attribut"); // cast de XAttribut
}

XPath

Csharp.svg
using System.Xml.XPath;

var noeud2 = xDoc.XPathSelectElement("/noeud1/noeud2");
var noeud2innerText = noeud2.Value;

IEnumerable<XElement> elements = xDoc.XPathSelectElements("noeud1 | noeud2");

IEnumerable attributs = (IEnumerable)xDoc.XPathEvaluate("/noeud/@nom_attribut");
var attribut = attributs.Cast<XAttribute>().FirstOrDefault();

// gestion des namespaces
var xmlNamespaceManager = new XmlNamespaceManager(new NameTable());
xmlNamespaceManager.AddNamespace("prefix", "uri");
var noeud2 = xDoc.XPathSelectElement("/noeud1/prefix:noeud2", xmlNamespaceManager);

Doc XPath

Écriture

Csharp.svg
var xDoc = new XDocument(
    new XDeclaration("1.0", "utf-8", "yes"),
    new XComment("Commentaire XML"),
    new XElement("contacts",
        new XElement("contact",
            new XElement("name", "Nicolas"),
            new XElement("phone", 
                new XAttribute("type", "mobile"),
                "06-...")),
        new XElement("contact",
            new XElement("name", "Audrey"),
            new XElement("phone", "01-..."))));


var xDoc = new XDocument();
xDoc.Declaration = new XDeclaration("1.0", "utf-8", "yes");
            
xDoc.Add(new XComment("Commentaire XML"));

var xContacts = new XElement("contacts");
xDoc.Root.Add(xContacts);

var xContactNicolas = new XElement("contact");
xContacts.Add(xContactNicolas);
xContactNicolas.SetElementValue("name", "Nicolas");
// équivalent à 
xContactNicolas.Add(new XElement("name", "Nicolas"));

var xPhone = new XElement("phone");
xContactNicolas.Add(xPhone);
xPhone.SetAttributeValue("type", "mobile");
// équivalent à 
xPhone.Add(new XAttribute("type", "mobile"));
xPhone.SetValue("06-...");
            
var xContactAudrey = new XElement("contact");
xContacts.Add(xContactAudrey);
xContactAudrey.Add(new XElement("name", "Audrey"));
xContactAudrey.Add(new XElement("phone", "01-..."));
Xml.svg
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<!--Commentaire XML -->
<contacts>
    <contact>
        <name>Nicolas</name>
        <phone type="mobile">06-...</phone>
    </contact>  
    <contact>
        <name>Audrey</name>
        <phone>01-...</phone>
    </contact>
</contacts>

XElement.Remove() et foreach

Csharp.svg
foreach (XElement xElt in xDoc.Root.Elements()) 
{
    // PB : après l'appel de Remove, on sort du foreach
    xElt.Remove();
}

// Solution 1 : utiliser LINQ
xDoc.Root.Elements().Remove();

// Solution 2 : parcourir à l’envers
foreach (XElement xElt in xDoc.Root.Elements().Reverse()) 
{
    xElt.Remove();
}

// Solution 3 : créer une liste temporaire d'éléments à supprimer

XElement.SetAttributeValue

  • Assigne la valeur à l'attribut spécifié.
  • Créer l'attribut s'il n'existe pas.
  • Si la valeur est null l'attribut est supprimé.
Csharp.svg
// code du Framework .NET
public void SetAttributeValue(XName name, object value)
{
    XAttribute xAttribute = this.Attribute(name);
    if (value == null)
    {
        if (xAttribute != null)
        {
            this.RemoveAttribute(xAttribute);
            return;
        }
    }
    else
    {
        if (xAttribute != null)
        {
            xAttribute.Value = XContainer.GetStringValue(value);
            return;
        }
        this.AppendAttribute(new XAttribute(name, value));
    }
}

Cast

Nativement les objets XAttribut et XElement peuvent être castés en :

  • DateTime, DateTimeOffset, TimeSpan
  • Decimal, Double, Int32, UInt32, Int64, UInt64
  • Guid
  • String
  • Boolean

Remarque : pour les XElement c'est la valeur du noeud ou la concaténation des valeurs de ses sous-noeuds qui sera casté.

Jointure

Csharp.svg
var query = from n1 in xdoc.Root.Elements("noeud1")
            join n2 in xdoc.Root.Elements("noeud2")
            on (string)n1.Attribute("attribut") equals
               (string)n2.Attribute("attribut")
            select new
            {
                Propriété1 = n1.Attribute("p1"),
                Propriété2 = n2.Attribute("p2")
            };