LINQ to XML

Language-Integrated Query (LINQ) er en teknologi der tillader query-egenskaber direkte i C#. I query-form ligner det SQL i syntax og opbygning. Alternativet er method-syntax som vi ikke vil bruge tid på i dette indlæg. Til de interesserede kan forskellen mellem query og method syntax ses her: http://msdn.microsoft.com/en-us/library/bb397947.aspx. MS anbefaler at man bruger query syntax alle de steder man kan, frem for method syntax.

LINQ to XML er en del af LINQ, og er designet til at trække data ud af xml, fra eksempelvis en fil.

Vi skal bruge namespacet ”System.Xml.Linq”. Dette namespace indeholder klasser som XDocument, XElement, XAttribute og XNode. Det er nogle af disse vi bruger i vores query.

LINQ syntaksen minder meget om SQL og kan ses her:
http://msdn.microsoft.com/en-us/library/bb308959.aspx#linqoverview_topic5

Lad os springe ud i det.

Lad os først finde noget XML vi selektivt vil trække data ud af. Følgende er et log udtræk fra SVN:

<?xml version=”1.0″?>
<log>
  <logentry revision=”200″>
    <author>Lars</author>
    <date>2010-06-03T08:24:19.329726Z</date>
    <paths>
      <path action=”M”>/sti/Upload.ascx.cs</path>
      <path action=”D”>/sti/Projekt.csproj.user</path>
    </paths>
    <msg>Changes to upload path handling</msg>
  </logentry>
  <logentry revision=”199″>
    <author>Lars</author>
    <date>2010-05-31T14:30:23.080950Z</date>
    <paths>
      <path action=”M”>/sti/Database.mdf</path>
      <path action=”M”>/sti/Database.ldf</path>
    </paths>
    <msg>Shrinked database again</msg>
  </logentry>
  <logentry revision=”198″>
    <author>Lars</author>
    <date>2010-05-31T13:59:33.689391Z</date>
    <paths>
      <path action=”A”>/sti/Projekt.sln</path>
      <path action=”A”>/sti/Projekt.suo</path>
    </paths>
    <msg>VS2010 Solution files.</msg>
  </logentry>
  <logentry revision=”197″>
    <author>Sral</author>
    <date>2010-05-28T07:10:51.597195Z</date>
    <paths>
      <path action=”M”>/sti/</path>
    </paths>
    <msg>Added files/folders to ignore filter.</msg>
  </logentry>
</log>

Først tæller vi alle log entries grupperet på author. Det kan klares således:

var output = from logentry in SvnLog.Elements("logentry")
    group logentry by (string) logentry.Element("author")
    into logentries
    select new { 
        Author = logentries.Key,
        Entries = logentries.Count()
    };

Vi piller querien fra hinanden og ser på de enkelte dele.

from logentry in SvnLog.Elements("logentry")

Dette statement returnerer en IEnumerable liste af XElementer indeholdende alle under-noder i hvert <logentry> element. Disse bliver så placeret i en variabel kaldet “logentry”.

group logentry by (string) logentry.Element("author") into logentries

Vi grupperer her på datasættet fra før med en under-node i <logentry> ved navn <author>. Note: Det er vigtigt at huske castet til string da de fleste klasser i System.Xml.Linq har custom type converters (http://msdn.microsoft.com/en-us/library/ayybcxe5.aspx).

Et cast til string giver elementets værdi, hvor logentry.Element(“author”).ToString() ville give en string repræsentation af objektet.

Til sidst placerer vi vores gruppering i en ny variable ved navn ”logentries”.

select new { Author = logentries.Key, Entries = logentries.Count() };

Her over laver vi en anonym type indeholdende vores fundne data (Anonymous Types: http://msdn.microsoft.com/en-us/library/bb397696.aspx)
Logentries.Key er altid hvad man grupperer på. I dette eksempel laver vi en simpel count på de fundne værdier.

Vi kører det igennem en foreach løkke:

foreach (var d in output) {
     Console.WriteLine("{0}: {1} entries.", d.Author, d.Entries);
}

Resultat:

Lars: 3 entries.
Sral: 1 entries.

Lad os prøve en lidt mere avanceret query med nestede selects.

Vi prøver følgende: Vores query skal tælle attributen action (i <path>) grupperet på værdien. For at være endnu mere vanskelig grupperer vi også på author igen.
Resultatet vi søger skulle gerne se sådan ud:

Lars: A:2, M:3, D:1
Sral: A:0, M:1, D:0

Querien kommer til at se således ud:

var data =
  from logentry
  in SvnLog.Elements("logentry")
  group logentry by (string)commit.Element("author")
    into logentries
    select new
    {
      Author = logentries.Key,
      Added = (
          from path in logentries.Elements("paths").Elements("path")
          where (string)path.Attribute("action") == "A"
          select path
      ).Count(),
      Modified = (
        from path in logentries.Elements("paths").Elements("path")
        where (string)path.Attribute("action") == "M"
        select path
      ).Count(),
      Deleted = (
        from path in logentries.Elements("paths").Elements("path")
        where (string)path.Attribute("action") == "D"
        select path
      ).Count()
    };

Det første i querien ligner statementet fra før: vi grupperer blot på author. Inde i vores anonyme type har vi nu 3 næsten ens selects:

from path in logentries.Elements("paths").Elements("path")

Vi finder alle ”path” noder som vores gruppering i det ydre scope indeholder.

where (string)path.Attribute("action") == "A"

Af de fundne path noder, finder vi alle dem der har attributten ”A”.

select path

Dem der matcher vores where clause selecter vi, plus pakker det hele ind til sidst, så vi kan lave en Count() på det.

foreach (var user in data) {
   Console.WriteLine("{0}: A:{1}, M:{2}, D:{3}",
      user.Author, user.Added, user.Modified, user.Deleted);
}

Og nu får vi det output vi søgte:

Lars: A:2, M:3, D:1
Sral: A:0, M:1, D:0