|
|
Ligne 256 : |
Ligne 256 : |
| === GroupBy + Sum === | | === GroupBy + Sum === |
| <kode lang='cs'> | | <kode lang='cs'> |
| var expensesSumByAccountId = expenses.GroupBy(x => x.AccountId)
| | expenses.GroupBy(x => x.AccountId) |
| .ToDictionary(x => x.Key, x => x.Sum(y => y.Amount));
| | .ToDictionary(x => x.Key, x => x.Sum(y => y.Amount)); |
| | |
| | expenses.GroupBy(x => x.AccountId) |
| | .Select(x => |
| | { |
| | AcountId = x.Key, |
| | TotalAmounts = x => x.Sum(y => y.Amount) |
| | }); |
| </kode> | | </kode> |
|
| |
|
Version du 28 novembre 2023 à 16:04
Assemblage
|
// Ajoutez au projet une référence à l'assemblage System.Core
using System.Linq;
|
Enumerable
Générer un énumérable
|
var UnACent = Enumerable.Range(1, 100); // 1 2 ... 100
var DixFoisSept = Enumerable.Repeat(7, 10); // 7 7 ... 7
|
Retourner un énumérable vide
|
return Enumerable.Empty<string>();
|
|
// afficher le contenu de MyTable
MyTable.Dump();
|
|
LinqPad permet d'obtenir l'équivalent SQL d'une requête LINQ. |
Union – Intersect – Except
|
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
|
Select
|
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
|
Select async
|
var result = await Task.WhenAll(myEnumerable.Select(MyMethodAsync));
// WhenAll extension method
public static Task<T[]> WhenAll<T>(this IEnumerable<Task<T>> source)
{
return Task.WhenAll(source);
}
var result = await myEnumerable.Select(MyMethodAsync).WhenAll();
// SelectAsync extension method
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);
// System.Linq.Async and System.Interactive.Async (for more operators)
var result = await myEnumerable.ToAsyncEnumerable()
.SelectAwait(async x => await MyMethodAsync(x))
.ToListAsync();
|
SelectMany
Même chose que Select, mais aplatit les résultat en une seule énumération.
|
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;
// select the parent in the result
var groups = new List<Group>
{
new Group
{
Id = 1,
Users = { new User { Id = 1 }, new User { Id = 2 } }
},
new Group
{
Id = 2,
Users = { new User { Id = 3 }, new User { Id = 4 } }
}
};
var userIds = groups.SelectMany(x => x.Users).Select(x => x.Id).ToArray(); // 1, 2, 3, 4
var groupAndUserIds = groups.SelectMany(
x => x.Users,
(x, y) => new
{
groupId = x.Id,
userId = y.Id
}).ToArray(); // (1, 1), (1, 2), (2, 3), (2, 4)
|
SelectMany async
|
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
|
// tri croissant suivant la longueur des mots
string[] words = { "cherry", "apple", "blueberry" };
var sortedWords = words.OrderBy(w => w.Length);
var sortedWords = from w in words
orderby w.Length
select w;
// tri décroissant
double[] doubles = { 1.7, 2.3, 1.9, 4.1, 2.9 };
var sortedDoubles = doubles.OrderByDescending(d => d);
var sortedDoubles = from d in doubles
orderby d descending
select d;
// double tri 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 = digits.OrderBy(d => d.Length).ThenBy(d => d);
var sortedDigits = from d in digits
orderby d.Length, d
select d;
// OrderBy extension method to sort by string property name and specifying the ascending or descending order
private static IEnumerable<T> OrderBy<T>(
this IEnumerable<T> source,
string propertyName,
bool ascendingOrder)
{
var propertyDescriptor = TypeDescriptor.GetProperties(typeof(T))
.Find(propertyName, false);
return ascendingOrder ?
source.OrderBy(x => propertyDescriptor.GetValue(x)) :
source.OrderByDescending(x => propertyDescriptor.GetValue(x));
}
|
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>.
|
// 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é
|
// 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
|
expenses.GroupBy(x => x.AccountId)
.ToDictionary(x => x.Key, x => x.Sum(y => y.Amount));
expenses.GroupBy(x => x.AccountId)
.Select(x =>
{
AcountId = x.Key,
TotalAmounts = x => x.Sum(y => y.Amount)
});
|
Flatten
|
// IEnumerable<IGrouping<char, <string>> → IEnumerable<char, string>
IEnumerable<char, string> result = words.GroupBy(w => w[0])
.SelectMany(x => x.Select(y => new { Letter = x.Key, Word = y }));
// { (b, blueberry), (b, banana), (c, chimpanzee), ... }
|
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>>
|
// 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
|
// 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();
|
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).
|
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> :
|
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
|
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"
// sum with seed of 0 in case of intArray is empty to avoid "sequence contains no elements" exception
var sum = intArray.Aggregate(0, (current, next) => current + next);
// concat array of strings into 1 string separated by new line
stringArray.Aggregate(new StringBuilder(), (current, next) => current.AppendLine(next)).ToString();
|
|
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
|
- si la classe implémente IEquatable<T>, Equals(T) est utilisé
- sinon les surcharges de Equals et GetHashCode de l'objet sont utilisés
- sinon il est possible de spécifier un EqualityComparer<T>
|
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
|
|
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);
}
}
|
|
public class ProductEqualityComparer : IEqualityComparer<Product>
{
public bool Equals(Product p1, Product p2)
{
if (ReferenceEquals(null, p1) || ReferenceEquals(null, p2))
{
return false;
}
if (ReferenceEquals(p1, p2))
{
return true;
}
return p1.Prop1 == p2.Prop1 && p1.Prop2 == p2.Prop2; // implement your own equality condition
}
public int GetHashCode(Product p)
{
if (p == null)
{
return 0;
}
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;
}
}
}
|
Applique une fonction aux éléments de deux séquences pour produire une séquence de résultats.
|
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
|
Task<IReadOnlyCollection<T>>
Use Task<IReadOnlyCollection<T>> when you can return as it is the result of the async call.
|
private static async Task<IReadOnlyCollection<string>> GetKeysAsync()
{
await Task.Delay(1000);
return new[] {
"Key1",
"Key2"
};
}
var keys = await GetKeysAsync();
|
IAsyncEnumerable<T>
|
private static async IAsyncEnumerable<string> GetKeysAsync()
{
var keys = await GetKeysAsync();
// here we have some changes on the result of the async call before the result would be sent.
var keyGroups = keys.GroupBy(x => x.Last());
foreach (var keyGroup in keyGroups)
{
yield return $"{keyGroup.Key} - {keyGroup}";
}
}
// to enumerate an IAsyncEnumerable<T>
await foreach (var key in GetKeysAsync())
{ }
var keys = await GetKeysAsync().ToListAsync();
|
Met en corrélation les éléments de deux séquences en fonction des clés qui correspondent.
|
// join a list of users and a list of groups
// pair them arround the matching between user.GroupId and group.Id
var query = users.Join(groups,
user => user.GroupId,
group => group.Id,
(user, group) =>
new
{
UserName = user.Name,
GroupName = group.Name
});
var query = from u in users
join g in groups
on u.GroupId equals g.Id
select new
{
UserName = u.Name,
GroupName = g.Name
};
|
|
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
|
<DataGrid ItemsSource="{Binding Result}" />
|
|
private IEnumerable _result;
public IEnumerable Result
{
get
{
return _result;
}
set
{
_result = value;
OnPropertyChanged("Result");
}
}
/* ... */
Result = query;
|
Links
É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é
Query examples
|
// filtering: where and or
from user in context.Users
where user.Age > 30 && (user.Name == "Nicolas" || user.Name == "Guillaume")
select user;
// ordering
from user in context.Users
orderby user.Name ascending, user.Age descending
select user;
// grouping: IEnumerable<IGrouping<int, User>>
from user in context.Users
group user by user.Age into g
order by g.Key
select g;
// joining
from user in context.Users
join group in context.Groups
on user.GroupId equals group.Id
select new { UserName = user.Name, GroupName = group.Name };
|
|
// use find if the condition is part of the primary key
var item = await this.DbContext.Set<Item>().FindAsync(cancellationToken, id);
// return null if no item with the same id is found in the context or in the database
|
Misc
- View → Server Explorer → Data Connections → Add Connection
- Add → Class → Data → LINQ to SQL Classes (fichier *.dbml)
- Glisser les tables dans le designer
|
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;
|
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.
|
// 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
|
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
|
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 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
|
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é.
|
// 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
|
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")
};
|