Chapter 11 - Other XML and JSON Technologies
Create sample files
// This creates the sample files used in Chapter 11 for XML.
// This query is #load-ed by other queries, so you don't need to run it directly.
// The files are written to the current (temp) folder, so they're cleaned up when you exit LINQPad
var contacts = @"<?xml version=""1.0"" encoding=""utf-8""?>
<contacts>
<customer id=""1"">
<firstname>Sara</firstname>
<lastname>Wells</lastname>
</customer>
<customer id=""2"">
<firstname>Dylan</firstname>
<lastname>Lockwood</lastname>
</customer>
<supplier>
<name>Ian Co.</name>
</supplier>
</contacts>";
if (!File.Exists ("contacts.xml")) File.WriteAllText ("contacts.xml", contacts);
var customer = @"<?xml version=""1.0"" encoding=""utf-8"" standalone=""yes""?>
<customer id=""123"" status=""archived"">
<firstname>Jim</firstname>
<lastname>Bo</lastname>
</customer>";
if (!File.Exists ("customer.xml")) File.WriteAllText ("customer.xml", customer);
var customerCredit = @"<?xml version=""1.0"" encoding=""utf-8"" standalone=""yes""?>
<customer id=""123"" status=""archived"">
<firstname>Jim</firstname>
<lastname>Bo</lastname>
<creditlimit>500.00</creditlimit> <!-- OK, we sneaked this in! -->
</customer>";
if (!File.Exists ("customerCredit.xml")) File.WriteAllText ("customerCredit.xml", customerCredit);
var customerWithCDATA = @"<?xml version=""1.0"" encoding=""utf-8"" ?>
<!DOCTYPE customer [ <!ENTITY tc ""Top Customer""> ]>
<customer id=""123"" status=""archived"">
<firstname>Jim</firstname>
<lastname>Bo</lastname>
<quote><![CDATA[C#'s operators include: < > &]]></quote>
<notes>Jim Bo is a &tc;</notes>
<!-- That wasn't so bad! -->
</customer>";
if (!File.Exists ("customerWithCDATA.xml")) File.WriteAllText ("customerWithCDATA.xml", customerWithCDATA);
var customers = @"<?xml version=""1.0"" encoding=""utf-8"" standalone=""yes""?>
<customers>
<customer id=""123"" status=""archived"">
<firstname>Jim</firstname>
<lastname>Bo</lastname>
</customer>
<customer id=""125"" status=""archived"">
<firstname>Todd</firstname>
<lastname>Bar</lastname>
</customer>
</customers>";
if (!File.Exists ("customers.xml")) File.WriteAllText ("customers.xml", customers);
var logfile = @"<?xml version=""1.0"" encoding=""utf-8""?>
<log>
<logentry id=""0""><date>2019-10-11T00:00:00-07:00</date><source>test</source></logentry>
<logentry id=""1""><date>2019-10-11T00:00:01-07:00</date><source>test</source></logentry>
<logentry id=""2""><date>2019-10-11T00:00:02-07:00</date><source>test</source></logentry>
<logentry id=""3""><date>2019-10-11T00:00:03-07:00</date><source>test</source></logentry>
<logentry id=""4""><date>2019-10-11T00:00:05-07:00</date><source>test</source></logentry>
<logentry id=""5""><date>2019-10-11T00:00:08-07:00</date><source>test</source></logentry>
<logentry id=""6""><date>2019-10-11T00:00:09-07:00</date><source>test</source></logentry>
<logentry id=""7""><date>2019-10-11T00:00:10-07:00</date><source>test</source></logentry>
<logentry id=""8""><date>2019-10-11T00:00:13-07:00</date><source>test</source></logentry>
<logentry id=""9""><date>2019-10-11T00:00:14-07:00</date><source>test</source></logentry>
</log>";
if (!File.Exists ("logfile.xml")) File.WriteAllText ("logfile.xml", logfile);
var customersSchema = @"<?xml version=""1.0"" encoding=""utf-8""?>
<xs:schema attributeFormDefault=""unqualified""
elementFormDefault=""qualified""
xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
<xs:element name=""customers"">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs=""unbounded"" name=""customer"">
<xs:complexType>
<xs:sequence>
<xs:element name=""firstname"" type=""xs:string"" />
<xs:element name=""lastname"" type=""xs:string"" />
</xs:sequence>
<xs:attribute name=""id"" type=""xs:int"" use=""required"" />
<xs:attribute name=""status"" type=""xs:string"" use=""required"" />
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
";
if (!File.Exists ("customers.xsd")) File.WriteAllText ("customers.xsd", customersSchema);
var customerXslt = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<xsl:stylesheet xmlns:xsl=""http://www.w3.org/1999/XSL/Transform""
version=""1.0"">
<xsl:template match=""/"">
<html>
<p><xsl:value-of select=""//firstname""/></p>
<p><xsl:value-of select=""//lastname""/></p>
</html>
</xsl:template>
</xsl:stylesheet>
";
if (!File.Exists ("customer.xslt")) File.WriteAllText ("customer.xslt", customerXslt);
Output XML Structure
#load ".\Create sample files.linq"
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreWhitespace = true;
using XmlReader reader = XmlReader.Create ("customer.xml", settings);
while (reader.Read())
{
Console.Write (new string (' ', reader.Depth * 2)); // Write indentation
Console.Write (reader.NodeType.ToString());
if (reader.NodeType == XmlNodeType.Element || reader.NodeType == XmlNodeType.EndElement)
Console.Write (" Name=" + reader.Name);
else if (reader.NodeType == XmlNodeType.Text)
Console.Write (" Value=" + reader.Value);
Console.WriteLine ();
}
EXTRA - Reading Nodes
#load ".\Create sample files.linq"
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreWhitespace = true;
settings.DtdProcessing = DtdProcessing.Parse; // Required to read DTDs
using XmlReader r = XmlReader.Create ("customerWithCDATA.xml", settings);
while (r.Read())
{
Console.Write (r.NodeType.ToString().PadRight (17, '-'));
Console.Write ("> ".PadRight (r.Depth * 3));
switch (r.NodeType)
{
case XmlNodeType.Element:
case XmlNodeType.EndElement:
Console.WriteLine (r.Name); break;
case XmlNodeType.Text:
case XmlNodeType.CDATA:
case XmlNodeType.Comment:
case XmlNodeType.XmlDeclaration:
Console.WriteLine (r.Value); break;
case XmlNodeType.DocumentType:
Console.WriteLine (r.Name + " - " + r.Value); break;
default: break;
}
}
Read Element Content As
#load ".\Create sample files.linq"
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreWhitespace = true;
using XmlReader r = XmlReader.Create ("customerCredit.xml", settings);
r.MoveToContent(); // Skip over the XML declaration
r.ReadStartElement ("customer");
string firstName = r.ReadElementContentAsString ("firstname", "");
string lastName = r.ReadElementContentAsString ("lastname", "");
decimal creditLimit = r.ReadElementContentAsDecimal ("creditlimit", "");
r.MoveToContent(); // Skip over that pesky comment
r.ReadEndElement(); // Read the closing customer tag
$"{firstName} {lastName} credit limit: {creditLimit}".Dump();
XML Writer
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
using XmlWriter writer = XmlWriter.Create ("foo.xml", settings);
writer.WriteStartElement ("customer");
writer.WriteAttributeString ("id", "1");
writer.WriteAttributeString ("status", "archived");
writer.WriteElementString ("firstname", "Jim");
writer.WriteElementString ("lastname", "Bo");
writer.WriteEndElement();
writer.Dispose();
var xml = File.ReadAllText("foo.xml");
xml.Dump();
XML Writer Namespace
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
using XmlWriter writer = XmlWriter.Create ("foo.xml", settings);
writer.WriteStartElement ("o", "customer", "http://oreilly.com");
writer.WriteElementString ("o", "firstname", "http://oreilly.com", "Jim");
writer.WriteElementString ("o", "lastname", "http://oreilly.com", "Bo");
writer.WriteEndElement();
writer.Dispose();
var xml = File.ReadAllText("foo.xml");
xml.Dump();
Serlialize to XML
void Main()
{
SerializeContacts();
DeserializeContacts().Dump();
}
private void SerializeContacts()
{
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true; // To make visual inspection easier
using XmlWriter writer = XmlWriter.Create ("contacts.xml", settings);
Contacts cts = new Contacts()
{
Customers = new List<Customer>()
{
new Customer() { ID = 1, FirstName = "Sara", LastName = "Wells"},
new Customer() { ID = 2, FirstName = "Dylan", LastName = "Lockwood"}
},
Suppliers = new List<Supplier>()
{
new Supplier() { Name = "Ian Weemes" }
}
};
writer.WriteStartElement ("contacts");
cts.WriteXml (writer);
writer.WriteEndElement();
}
private Contacts DeserializeContacts()
{
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreWhitespace = true;
settings.IgnoreComments = true;
settings.IgnoreProcessingInstructions = true;
using XmlReader reader = XmlReader.Create ("contacts.xml", settings);
reader.MoveToContent();
var cts = new Contacts();
cts.ReadXml (reader);
return cts;
}
public class Contacts
{
public IList<Customer> Customers = new List<Customer>();
public IList<Supplier> Suppliers = new List<Supplier>();
public void ReadXml (XmlReader r)
{
bool isEmpty = r.IsEmptyElement; // This ensures we don't get
r.ReadStartElement(); // snookered by an empty
if (isEmpty) return; // <contacts/> element!
while (r.NodeType == XmlNodeType.Element)
{
if (r.Name == Customer.XmlName) Customers.Add (new Customer (r));
else if (r.Name == Supplier.XmlName) Suppliers.Add (new Supplier (r));
else
throw new XmlException ("Unexpected node: " + r.Name);
}
r.ReadEndElement();
}
public void WriteXml (XmlWriter w)
{
foreach (Customer c in Customers)
{
w.WriteStartElement (Customer.XmlName);
c.WriteXml (w);
w.WriteEndElement();
}
foreach (Supplier s in Suppliers)
{
w.WriteStartElement (Supplier.XmlName);
s.WriteXml (w);
w.WriteEndElement();
}
}
}
public class Customer
{
public const string XmlName = "customer";
public int? ID;
public string FirstName, LastName;
public Customer () { }
public Customer (XmlReader r) { ReadXml (r); }
public void ReadXml (XmlReader r)
{
if (r.MoveToAttribute ("id")) ID = r.ReadContentAsInt();
r.ReadStartElement();
FirstName = r.ReadElementContentAsString ("firstname", "");
LastName = r.ReadElementContentAsString ("lastname", "");
r.ReadEndElement();
}
public void WriteXml (XmlWriter w)
{
if (ID.HasValue) w.WriteAttributeString ("id", "", ID.ToString());
w.WriteElementString ("firstname", FirstName);
w.WriteElementString ("lastname", LastName);
}
}
public class Supplier
{
public const string XmlName = "supplier";
public string Name;
public Supplier () { }
public Supplier (XmlReader r) { ReadXml (r); }
public void ReadXml (XmlReader r)
{
r.ReadStartElement();
Name = r.ReadElementContentAsString ("name", "");
r.ReadEndElement();
}
public void WriteXml (XmlWriter w)
{
w.WriteElementString ("name", Name);
}
}
Reading with XElement
#load ".\Create sample files.linq"
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreWhitespace = true;
using XmlReader r = XmlReader.Create ("logfile.xml", settings);
r.ReadStartElement ("log");
while (r.Name == "logentry")
{
XElement logEntry = (XElement)XNode.ReadFrom (r);
int id = (int)logEntry.Attribute ("id");
DateTime date = (DateTime)logEntry.Element ("date");
string source = (string)logEntry.Element ("source");
$"{id} {date} {source}".Dump();
}
r.ReadEndElement();
Writing with XElement
XmlWriterSettings settings = new XmlWriterSettings() { Indent = true }; // Otherwise the XML is written as one very long line.
// Saves space but makes it more difficult for humans.
using XmlWriter w = XmlWriter.Create ("logfile.xml", settings);
w.WriteStartElement ("log");
for (int i = 0; i < 1000000; i++)
{
XElement e = new XElement ("logentry",
new XAttribute ("id", i),
new XElement ("date", DateTime.Today.AddDays (-1)),
new XElement ("source", "test"));
e.WriteTo (w);
}
w.WriteEndElement ();
w.Dispose();
using var reader = File.OpenText("logfile.xml");
for (int i = 0; i < 10; i++) Console.WriteLine (reader.ReadLine());
Validate with XSD
#load ".\Create sample files.linq"
XmlReaderSettings settings = new XmlReaderSettings();
settings.ValidationType = ValidationType.Schema;
settings.Schemas.Add (null, "customers.xsd");
using (XmlReader r = XmlReader.Create ("customers.xml", settings))
try { while (r.Read()) ; }
catch (XmlSchemaValidationException ex)
{
$"Invalid XML according to schema: {ex.Message}".Dump();
}
"Finished processing XML".Dump();
Transform with XSLT
#load ".\Create sample files.linq"
XslCompiledTransform transform = new XslCompiledTransform();
transform.Load ("customer.xslt");
transform.Transform ("customer.xml", "customer.xhtml");
File.ReadAllText("customer.xhtml").Dump();
Create sample JSON files
// This creates the sample files used in Chapter 11 for JSON.
// This query is #load-ed by other queries, so you don't need to run it directly.
// The files are cleaned up when you exit LINQPad
var people = @"{
""FirstName"":""Sara"",
""LastName"":""Wells"",
""Age"":35,
""Friends"":[""Dylan"",""Ian""]
}";
if (!File.Exists ("people.json")) File.WriteAllText ("people.json", people);
var peopleArray = @"[
{
""FirstName"":""Sara"",
""LastName"":""Wells"",
""Age"":35,
""Friends"":[""Ian""]
},
{
""FirstName"":""Ian"",
""LastName"":""Weems"",
""Age"":42,
""Friends"":[""Joe"",""Eric"",""Li""]
},
{
""FirstName"":""Dylan"",
""LastName"":""Lockwood"",
""Age"":46,
""Friends"":[""Sara"",""Ian""]
}
]";
if (!File.Exists ("peopleArray.json")) File.WriteAllText ("peopleArray.json", peopleArray);
JSON Reader
#load ".\Create sample JSON files.linq"
byte[] data = File.ReadAllBytes ("people.json");
Utf8JsonReader reader = new Utf8JsonReader (data);
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.StartObject:
Console.WriteLine ($"Start of object");
break;
case JsonTokenType.EndObject:
Console.WriteLine ($"End of object");
break;
case JsonTokenType.StartArray:
Console.WriteLine();
Console.WriteLine ($"Start of array");
break;
case JsonTokenType.EndArray:
Console.WriteLine ($"End of array");
break;
case JsonTokenType.PropertyName:
Console.Write ($"Property: {reader.GetString()}");
break;
case JsonTokenType.String:
Console.WriteLine ($" Value: {reader.GetString()}");
break;
case JsonTokenType.Number:
Console.WriteLine ($" Value: {reader.GetInt32()}");
break;
default:
Console.WriteLine ($"No support for {reader.TokenType}");
break;
}
}
JSON Writer
var options = new JsonWriterOptions { Indented = true };
using (var stream = File.Create ("MyFile.json"))
using (var writer = new Utf8JsonWriter (stream, options))
{
writer.WriteStartObject();
// Property name and value specified in one call
writer.WriteString ("FirstName", "Dylan");
writer.WriteString ("LastName", "Lockwood");
// Property name and value specified in separate calls
writer.WritePropertyName ("Age");
writer.WriteNumberValue (46);
writer.WriteCommentValue ("This is a (non-standard) comment");
writer.WriteEndObject();
}
File.ReadAllText("MyFile.json").Dump();
JSON Document
Number();
Array();
Age();
void Number()
{
using JsonDocument document = JsonDocument.Parse ("123");
JsonElement root = document.RootElement;
Console.WriteLine (root.ValueKind); // Number
int number = document.RootElement.GetInt32();
Console.WriteLine (number); // 123
}
void Array()
{
using JsonDocument document = JsonDocument.Parse (@"[1, 2, 3, 4, 5]");
int length = document.RootElement.GetArrayLength(); // 5
int value = document.RootElement [3].GetInt32(); // 4
Console.WriteLine($"length: {length}; value {value}");
}
void Age()
{
using JsonDocument document = JsonDocument.Parse (@"{ ""Age"": 32}");
JsonElement root = document.RootElement;
int age = root.GetProperty ("Age").GetInt32();
Console.WriteLine(age);
// Discover Age property
JsonProperty ageProp = root.EnumerateObject().First();
string name = ageProp.Name; // Age
JsonElement value = ageProp.Value;
Console.WriteLine (value.ValueKind); // Number
Console.WriteLine (value.GetInt32()); // 32
}
LINQ with JSON
#load ".\Create sample JSON files.linq"
var jsonPath = "peopleArray.json";
using var json = File.OpenRead (jsonPath);
using JsonDocument document = JsonDocument.Parse (json);
var query =
from person in document.RootElement.EnumerateArray()
select new
{
FirstName = person.GetProperty ("FirstName").GetString(),
Age = person.GetProperty ("Age").GetInt32(),
Friends =
from friend in person.GetProperty ("Friends").EnumerateArray()
select friend.GetString()
};
query.Dump();
JsonDocument - Updating with a JSON writer
#load ".\Create sample JSON files.linq"
using JsonDocument document = JsonDocument.Parse (peopleArray);
var options = new JsonWriterOptions { Indented = true };
using (var stream = File.Create ("MyFile.json"))
using (var writer = new Utf8JsonWriter (stream, options))
{
writer.WriteStartArray();
foreach (var person in document.RootElement.EnumerateArray())
{
int friendCount = person.GetProperty ("Friends").GetArrayLength();
if (friendCount >= 2)
person.WriteTo (writer);
}
}
File.ReadAllText ("MyFile.json").Dump();