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);
}