Chapter 10 - LINQ to XML

X-DOM Overview

Getting Started

XElement config = XElement.Parse (
@"<configuration>
  <client enabled='true'>
    <timeout>30</timeout>
  </client>
</configuration>");

foreach (XElement child in config.Elements())
  child.Name.Dump ("Child element name");

XElement client = config.Element ("client");

bool enabled = (bool) client.Attribute ("enabled");   // Read attribute
enabled.Dump ("enabled attribute");

client.Attribute ("enabled").SetValue (!enabled);     // Update attribute

int timeout = (int) client.Element ("timeout");       // Read element
timeout.Dump ("timeout element");

client.Element ("timeout").SetValue (timeout * 2);    // Update element

client.Add (new XElement ("retries", 3));             // Add new elememt

config.Dump ("Updated DOM");
Instantiating an X-DOM

Imperative Construction

XElement lastName = new XElement ("lastname", "Bloggs");
lastName.Add (new XComment ("nice name"));

XElement customer = new XElement ("customer");
customer.Add (new XAttribute ("id", 123));
customer.Add (new XElement ("firstname", "Joe"));
customer.Add (lastName);

customer.Dump();

Functional Construction

new XElement ("customer",  new XAttribute ("id", 123),
  new XElement ("firstname", "joe"),
  new XElement ("lastname", "bloggs",
    new XComment ("nice name")
  )
)

Functional Construction from Database Query

new XElement ("customers",
  from c in Customers.AsEnumerable()
    select new XElement ("customer",
      new XAttribute ("id", c.ID),
      new XElement ("name", c.Name,
          new XComment ("nice name")
    )
  )
)

Automatic Deep Cloning

var address =
  new XElement ("address",
    new XElement ("street", "Lawley St"), 
    new XElement ("town", "North Beach")
  );
  
var customer1 = new XElement ("customer1", address);
var customer2 = new XElement ("customer2", address);

customer1.Element ("address").Element ("street").Value = "Another St";
Console.WriteLine (customer2.Element ("address").Element ("street").Value);
Navigating and Querying

FirstNode LastNode and Nodes

var bench =
  new XElement ("bench",
    new XElement ("toolbox",
      new XElement ("handtool", "Hammer"),
      new XElement ("handtool", "Rasp")
    ),
    new XElement ("toolbox",
      new XElement ("handtool", "Saw"), 
      new XElement ("powertool", "Nailgun")
    ),
    new XComment ("Be careful with the nailgun")
  );

bench.FirstNode.Dump ("FirstNode");
bench.LastNode.Dump ("LastNode");
    
foreach (XNode node in bench.Nodes())
  Console.WriteLine (node.ToString (SaveOptions.DisableFormatting) + ".");

Enumerating Elements

var bench =
  new XElement ("bench",
    new XElement ("toolbox",
      new XElement ("handtool", "Hammer"),
      new XElement ("handtool", "Rasp")
    ),
    new XElement ("toolbox",
      new XElement ("handtool", "Saw"), 
      new XElement ("powertool", "Nailgun")
    ),
    new XComment ("Be careful with the nailgun")
  );

foreach (XElement e in bench.Elements())
  Console.WriteLine (e.Name + "=" + e.Value);

Querying Elements

var bench =
  new XElement ("bench",
    new XElement ("toolbox",
      new XElement ("handtool", "Hammer"),
      new XElement ("handtool", "Rasp")
    ),
    new XElement ("toolbox",
      new XElement ("handtool", "Saw"), 
      new XElement ("powertool", "Nailgun")
    ),
    new XComment ("Be careful with the nailgun")
  );

var toolboxWithNailgun =
  from toolbox in bench.Elements()
  where toolbox.Elements().Any (tool => tool.Value == "Nailgun")
  select toolbox.Value;
  
var handTools =
  from toolbox in bench.Elements()
  from tool in toolbox.Elements()
  where tool.Name == "handtool"
  select tool.Value;

int toolboxCount = bench.Elements ("toolbox").Count();

var handTools2 =
  from tool in bench.Elements ("toolbox").Elements ("handtool")
  select tool.Value.ToUpper();

toolboxWithNailgun.Dump ("The toolbox with the nailgun");
handTools.Dump ("The hand tools in all toolboxes");
toolboxCount.Dump ("Number of toolboxes");
handTools2.Dump ("The hand tools in all toolboxes");

Querying Elements - Recursive

var bench =
  new XElement ("bench",
    new XElement ("toolbox",
      new XElement ("handtool", "Hammer"),
      new XElement ("handtool", "Rasp")
    ),
    new XElement ("toolbox",
      new XElement ("handtool", "Saw"), 
      new XElement ("powertool", "Nailgun")
    ),
    new XComment ("Be careful with the nailgun")
  );

bench.Descendants ("handtool").Count().Dump ("Count of all handtools");

foreach (XNode node in bench.DescendantNodes())
  Console.WriteLine (node.ToString (SaveOptions.DisableFormatting));

(  
  from c in bench.DescendantNodes().OfType<XComment>()
  where c.Value.Contains ("careful")
  orderby c.Value
  select c.Value
)
.Dump ("Comments anywhere in the X-DOM containing the word 'careful'");
Updating an X-DOM

SetValue Replaces Child Content

XElement settings =
  new XElement ("settings",
    new XElement ("timeout", 30)
  );

settings.Dump ("Original XML");  

settings.SetValue ("blah");
settings.Dump ("Notice the timeout node has disappeared");

SetElementValue

XElement settings = new XElement ("settings");

settings.SetElementValue ("timeout", 30);   settings.Dump ("Adds child element"); 
settings.SetElementValue ("timeout", 60);   settings.Dump ("Updates child element");

AddAfterSelf

XElement items =
  new XElement ("items",
    new XElement ("one"),
    new XElement ("three")
  );

items.Dump ("Original XML");
    
items.FirstNode.AddAfterSelf (new XElement ("two"));

items.Dump ("After calling items.FirstNode.AddAfterSelf");

ReplaceWith

XElement items = XElement.Parse (@"
<items>
  <one/><two/><three/>  
</items>");

items.Dump ("Original XML");

items.FirstNode.ReplaceWith (new XComment ("One was here"));

items.Dump ("After calling ReplaceWith");

Remove Extension Method

XElement contacts = XElement.Parse (@"
<contacts>
  <customer name='Mary'/>
  <customer name='Chris' archived='true'/>
  <supplier name='Susan'>
    <phone archived='true'>012345678<!--confidential--></phone>
  </supplier>
</contacts>");

contacts.Dump ("Before");

contacts.Elements ("customer").Remove();

contacts.Dump ("After");

Remove - Conditional

XElement contacts = XElement.Parse (@"
<contacts>
  <customer name='Mary'/>
  <customer name='Chris' archived='true'/>
  <supplier name='Susan'>
    <phone archived='true'>012345678<!--confidential--></phone>
  </supplier>
</contacts>");

contacts.Dump ("Before");

contacts.Elements()
  .Where (e => (bool?) e.Attribute ("archived") == true)
  .Remove();

contacts.Dump ("After");

Remove - Recursive

XElement contacts = XElement.Parse (@"
<contacts>
  <customer name='Mary'/>
  <customer name='Chris' archived='true'/>
  <supplier name='Susan'>
    <phone archived='true'>012345678<!--confidential--></phone>
  </supplier>
</contacts>");

contacts.Dump ("Before");

contacts.Descendants()
  .Where (e => (bool?) e.Attribute ("archived") == true)
  .Remove();

contacts.Dump ("After");

Remove - Recursive OfType

XElement contacts = XElement.Parse (@"
<contacts>
  <customer name='Mary'/>
  <customer name='Chris' archived='true'/>
  <supplier name='Susan'>
    <phone archived='true'>012345678<!--confidential--></phone>
  </supplier>
</contacts>");

contacts.Dump ("Before");

contacts.Elements()
  .Where (
    e => e.DescendantNodes().OfType<XComment>().Any (c => c.Value == "confidential")
  )
  .Remove();
                          
contacts.Dump ("After");
Working with Values

Setting Values

var e = new XElement ("date", DateTime.Now);
e.SetValue (DateTime.Now.AddDays(1));
e.Value.Dump();

Getting Values

XElement e = new XElement ("now", DateTime.Now);
DateTime dt = (DateTime) e;

XAttribute a = new XAttribute ("resolution", 1.234);
double res = (double) a;

dt.Dump();
res.Dump();

Getting Values - Nullables

var x = new XElement ("Empty");

try
{
  int timeout1 = (int) x.Element ("timeout");
}
catch (Exception ex)
{
  ex.Message.Dump ("Element (\"timeout\") returns null so the result cannot be cast to int");
}

int? timeout2 = (int?) x.Element ("timeout");
timeout2.Dump ("Casting to a nullable type solve this problem");

Factoring out nullable types

var x = new XElement ("Empty");

double resolution = (double?) x.Attribute ("resolution") ?? 1.0;

resolution.Dump();

Value casts in LINQ queries

var data = XElement.Parse (@"
<data>
  <customer id='1' name='Mary' credit='100' />
  <customer id='2' name='John' credit='150' />
  <customer id='3' name='Anne' />
</data>");
  
IEnumerable<string> query =
  from cust in data.Elements()
  where (int?) cust.Attribute ("credit") > 100
  select cust.Attribute ("name").Value;
  
query.Dump();

Value and Mixed Content Nodes

XElement summary =
  new XElement ("summary",
    new XText ("An XAttribute is "),
    new XElement ("bold", "not"),
    new XText (" an XNode")
  );

summary.Dump();

XText Concatenation

var e1 = new XElement ("test", "Hello");
e1.Add ("World");

var e2 = new XElement ("test", "Hello", "World");

var e3 = new XElement ("test", new XText ("Hello"), new XText ("World"));

e1.Dump(); e2.Dump(); e3.Dump();

e1.Nodes().Count().Dump ("Number of children in e1");
e2.Nodes().Count().Dump ("Number of children in e2");
e3.Nodes().Count().Dump ("Number of children in e3");
Documents and Declarations

Simplest Valid XDocument

new XDocument (
  new XElement ("test", "data")
)

Building an XHTML document

var styleInstruction = new XProcessingInstruction (
  "xml-stylesheet", "href='styles.css' type='text/css'"
);

var docType = new XDocumentType ("html",
  "-//W3C//DTD XHTML 1.0 Strict//EN",
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd", null);

XNamespace ns = "http://www.w3.org/1999/xhtml";
var root =
  new XElement (ns + "html",
    new XElement (ns + "head",
      new XElement (ns + "title", "An XHTML page")),
    new XElement (ns + "body",
      new XElement (ns + "h1", "This is a heading."),
      new XElement (ns + "p", "This is some content."))
  );
  
var doc =
  new XDocument (
    new XDeclaration ("1.0", "utf-8", "no"),
    new XComment ("Reference a stylesheet"),
    styleInstruction,
    docType,
    root
  );

string tempPath = Path.Combine (Path.GetTempPath(), "sample.html");
doc.Save (tempPath);

// This will display the page in IE or FireFox
Process.Start (new ProcessStartInfo (tempPath) { UseShellExecute = true });
File.ReadAllText (tempPath).Dump();

doc.Root.Name.LocalName.Dump ("Root element's local name");

XElement bodyNode = doc.Root.Element (ns + "body");
(bodyNode.Document == doc).Dump ("bodyNode.Document == doc");

(doc.Root.Parent == null).Dump ("doc.Root.Parent is null");

foreach (XNode node in doc.Nodes())
  Console.Write (node.Parent == null);

Declarations

var doc =
  new XDocument (
    new XDeclaration ("1.0", "utf-16", "yes"),
    new XElement ("test", "data")
  );

string tempPath = Path.Combine (Path.GetTempPath(), "test.xml");
doc.Save (tempPath);
File.ReadAllText (tempPath).Dump();

Writing a Declaration to a String

var doc =
  new XDocument (
    new XDeclaration ("1.0", "utf-8", "yes"),
    new XElement ("test", "data")
  );

var output = new StringBuilder();
var settings = new XmlWriterSettings { Indent = true };

using (XmlWriter xw = XmlWriter.Create (output, settings))
  doc.Save (xw);

output.ToString().Dump ("Notice the encoding is utf-16 and not utf-8");
Names and Namespaces

Specifying a Namespace with Braces

new XElement ("{http://domain.com/xmlspace}customer", "Bloggs")

XName and XNamespace

XName localName = "customer";
XName fullName1 = "{http://domain.com/xmlspace}customer";
fullName1.Dump ("fullname1");

XNamespace ns = "http://domain.com/xmlspace";
XName fullName2 = ns + "customer";
fullName2.Dump ("fullname2 - same result, but cleaner and more efficient");

Namespaces and Attributes

XNamespace ns = "http://domain.com/xmlspace";

var data =
  new XElement (ns + "data",
    new XAttribute (ns + "id", 123)
  );
  
data.Dump();

Default Namespaces

XNamespace ns = "http://domain.com/xmlspace";

var data =
  new XElement (ns + "data",
    new XElement (ns + "customer", "Bloggs"),
    new XElement (ns + "purchase", "Bicycle")
  );  
  
data.Dump ("The whole DOM");

data.Element (ns + "customer").Dump ("The customer element (notice namespace is now present)");

Forgetting the Namespace

XNamespace ns = "http://domain.com/xmlspace";

var data =
  new XElement (ns + "data",
    new XElement ("customer", "Bloggs"),
    new XElement ("purchase", "Bicycle")
  );
  
data.Dump ("Forgetting to specify namespaces in construction");

data = 
  new XElement (ns + "data",
    new XElement (ns + "customer", "Bloggs"),
    new XElement (ns + "purchase", "Bicycle")
  );
  
XElement x = data.Element (ns + "customer");    // OK
XElement y = data.Element ("customer");

y.Dump ("Forgetting to specify a namespace when querying");

Assigning Empty Namespaces

XNamespace ns = "http://domain.com/xmlspace";

var data =
  new XElement (ns + "data",
    new XElement ("customer", "Bloggs"),
    new XElement ("purchase", "Bicycle")
  );
  
data.Dump ("Before");
  
foreach (XElement e in data.DescendantsAndSelf())
  if (e.Name.Namespace == "")
    e.Name = ns + e.Name.LocalName;

data.Dump ("After");

Specifing Prefixes

XNamespace ns1 = "http://domain.com/space1";
XNamespace ns2 = "http://domain.com/space2";

var mix =
  new XElement (ns1 + "data",
    new XElement (ns2 + "element", "value"),
    new XElement (ns2 + "element", "value"),
    new XElement (ns2 + "element", "value")
  );

mix.Dump ("Without prefixes");

mix.SetAttributeValue (XNamespace.Xmlns + "ns1", ns1);
mix.SetAttributeValue (XNamespace.Xmlns + "ns2", ns2);

mix.Dump ("With prefixes");

Prefixes and Attributes

XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance";
var nil = new XAttribute (xsi + "nil", true);

var cust =
  new XElement ("customers",
    new XAttribute (XNamespace.Xmlns + "xsi", xsi),
    new XElement ("customer",
      new XElement ("lastname", "Bloggs"),
      new XElement ("dob", nil),
      new XElement ("credit", nil)
    )
  );

cust.Dump();
Annotations

Using Annotations

XElement e = new XElement ("test");

e.AddAnnotation ("Hello");
e.Annotation<string>().Dump ("String annotations");

e.RemoveAnnotations<string>();
e.Annotation<string>().Dump ("String annotations");

Annotations with Custom Types

void Main()
{
  XElement e = new XElement ("test");
  
  e.AddAnnotation (new CustomData { Message = "Hello" } );
  e.Annotations<CustomData>().First().Message.Dump();
  
  e.RemoveAnnotations<CustomData>();
  e.Annotations<CustomData>().Count().Dump();  
}

class CustomData        // Private nested type
{
   internal string Message;
}
Projecting into an X-DOM

Step 1 - Functional Construction with Literals

var customers =
  new XElement ("customers",
    new XElement ("customer", new XAttribute ("id", 1),
      new XElement ("name", "Sue"),
      new XElement ("buys", 3) 
    )
  );
  
customers.Dump();

Step 2 - Build a Projection Around it

var customers =
  new XElement ("customers",
    // The AsEnumerable call can be removed when the EF Core bug is fixed.
    from c in Customers.AsEnumerable()
    select 
      new XElement ("customer", new XAttribute ("id", c.ID),
        new XElement ("name", c.Name),
        new XElement ("buys", c.Purchases.Count)
      )
    );
  
customers.Dump();

Same Query Built Progressively

var sqlQuery =
  from c in Customers.AsEnumerable()
    select 
      new XElement ("customer", new XAttribute ("id", c.ID),
        new XElement ("name", c.Name),
        new XElement ("buys", c.Purchases.Count)
      );
  
var customers = new XElement ("customers", sqlQuery);

sqlQuery.Dump ("SQL Query");
customers.Dump ("Final projection");

Eliminating Empty Elements - Problem

new XElement ("customers",
  // The call to AsEnumerable can be removed when the EF Core bug is fixed.
  from c in Customers.AsEnumerable()
    let lastBigBuy = (
      from p in c.Purchases
      where p.Price > 1000
      orderby p.Date descending
      select p
    ).FirstOrDefault()
  select 
    new XElement ("customer", new XAttribute ("id", c.ID),
      new XElement ("name", c.Name),
      new XElement ("buys", c.Purchases.Count),
      new XElement ("lastBigBuy",
        new XElement ("description",
          lastBigBuy == null ? null : lastBigBuy.Description),
        new XElement ("price",
          lastBigBuy == null ? 0m : lastBigBuy.Price)
        )
      )
    )

Eliminating Empty Elements - Solution

new XElement ("customers",
  // The call to AsEnumerable can be removed when the EF Core bug is fixed.
  from c in Customers.AsEnumerable()
    let lastBigBuy = (
      from p in c.Purchases
      where p.Price > 1000
      orderby p.Date descending
      select p
    ).FirstOrDefault()
  select 
    new XElement ("customer", new XAttribute ("id", c.ID),
      new XElement ("name", c.Name),
      new XElement ("buys", c.Purchases.Count),
      lastBigBuy == null ? null : 
        new XElement ("lastBigBuy",
          new XElement ("description", lastBigBuy.Description),
          new XElement ("price", lastBigBuy.Price)
        )
      )
    )

Streaming a Projection

new XStreamingElement ("customers",
  from c in Customers
    select 
      new XStreamingElement ("customer", new XAttribute ("id", c.ID),
        new XElement ("name", c.Name),
        new XElement ("buys", c.Purchases.Count)
      )
    )

EXTRA - Transforming an X-DOM

XElement project = XElement.Parse (@"<Project Sdk=""Microsoft.NET.Sdk"">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <Authors>Joe Bloggs</Authors>
    <Version>1.1.42</Version>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include=""Microsoft.EntityFrameworkCore"" 
            Version=""3.0.0"" />
    <PackageReference Include=""Microsoft.EntityFrameworkCore.Proxies"" 
            Version=""3.0.0"" />
    <PackageReference Include=""Microsoft.EntityFrameworkCore.SqlServer"" 
            Version=""3.0.0"" />
    <PackageReference Include=""Newtonsoft.Json"" 
            Version=""12.0.2"" />
  </ItemGroup>

</Project>
");

var query =
  new XElement ("DependencyReport",
    from compileItem in
      project.Elements ("ItemGroup").Elements ("PackageReference")
    let include = compileItem.Attribute ("Include")
    where include != null
    select new XElement ("Dependency", include.Value)
  );

query.Dump();

EXTRA - Advanced Transformations

void Main()
{
  XElement project = XElement.Parse (@"<Project Sdk=""Microsoft.NET.Sdk"">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <Authors>Joe Bloggs</Authors>
    <Version>1.1.42</Version>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include=""Microsoft.EntityFrameworkCore"" 
            Version=""3.0.0"" />
    <PackageReference Include=""Microsoft.EntityFrameworkCore.Proxies"" 
            Version=""3.0.0"" />
    <PackageReference Include=""Microsoft.EntityFrameworkCore.SqlServer"" 
            Version=""3.0.0"" />
    <PackageReference Include=""Newtonsoft.Json"" 
            Version=""12.0.2"" />
  </ItemGroup>

</Project>
");

 IEnumerable<string> depNames =
  from compileItem in
         project.Elements ("ItemGroup").Elements ("PackageReference")
  let include = compileItem.Attribute ("Include")
  where include != null
  select include.Value;
  
  var query = new XElement ("Project", CreateHierarchy (depNames ));
  query.Dump();
}

static IEnumerable<XElement> CreateHierarchy (IEnumerable<string> depName)
{
  var brokenUp = from path in depName
                   let split = path.Split (new char[] { '.' }, 2)
                   orderby split [0]
                   select new
                   {
                     name = split [0],
                     remainder = split.ElementAtOrDefault (1)
                   };

  IEnumerable<XElement> pkg = from b in brokenUp
                                where b.remainder == null
                                select new XElement ("pkg", b.name);

  IEnumerable<XElement> parts = from b in brokenUp
                                  where b.remainder != null
                                  group b.remainder by b.name into grp
                                  select new XElement ("part",
                                    new XAttribute ("name", grp.Key),
                                    CreateHierarchy (grp)
                                  );
  return pkg.Concat (parts);
}
C# 12 in a Nutshell
Buy from amazon.com Buy print or Kindle edition
Buy from ebooks.com Buy PDF edition
Buy from O'Reilly Read via O'Reilly subscription