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 your default browser
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);
}