One of new features introduced in .NET 3.5 that I welcomed the most was LINQ to XML. The old DOM API was a bit clumsy to use, and the simple fact that you don't need owner documents any more makes the new XElement much more flexible and pleasant to work with than the old XmlElement.
Also new is an API for streaming XML output, XStreamingElement, that by using deferred execution gives you SAX-like performance on a DOM-like API. There's no streaming XML input API, though, so although you can now get away with not having to use XmlWriter any more, you'll still need to use XmlReader when you want good performance on large documents. During the LINQ to XML development, the XML Team considered such an API, but they decided not to do it for Orcas.
Although XElementReader looks a lot like a subset of XElement, you still have to remember that it's using a XmlReader underneath, so after you try to get to any child element, you have changed the reader position.
For example, if you have an element like <Root><A/><B/><A/><B/></Root> and call .Element("A") twice and then .Element("B") twice, the second call for B will return null. If you instead call .Elements("A") and then .Elements("B"), you'll get two A elements, but no B elements at all. So to do this right, you have iterate on .Elements() and check the .Name property to see if you're in an A or in a B element.
This read once nature sometimes also complicates debugging. To help with that, you can define XML_DEBUG_MODE to force XElementReader to use a XElement behind the covers instead of a XmlReader, so you can add watches freely without worrying about side effects. But remember to fully test with this conditional compilation symbol off.
using System;
using System.Collections.Generic;
using System.IO;
#if XML_DEBUG_MODE
using System.Linq;
#endif
using System.Xml;
using System.Xml.Linq;
public class XElementReader : IDisposable {
private int targetDepth;
#if XML_DEBUG_MODE
private readonly XElement elem;
private XElementReader(XElement elem, int depth) {
this.elem = elem;
this.targetDepth = depth;
}
public XElementReader(string path) {
this.elem = XElement.Load(path);
}
public XElementReader(TextReader reader) {
this.elem = XElement.Load(reader);
}
public XElementReader(XmlReader reader) {
this.elem = XElement.Load(reader);
}
public XElementReader(Stream stream) {
this.elem = XElement.Load(XmlReader.Create(stream));
}
public void Close() {
if (targetDepth != 0) {
throw new InvalidOperationException("Only the outermost reader should be closed");
}
}
public XName Name {
get { return elem.Name; }
}
public string Value {
get { return elem.Value; }
}
public XAttribute Attribute(XName name) {
return elem.Attribute(name);
}
public IEnumerable<XAttribute> Attributes() {
return elem.Attributes();
}
public XElementReader FirstElement() {
XElement subElem = elem.Elements().FirstOrDefault();
return subElem == null ? null : new XElementReader(subElem, targetDepth + 1);
}
public XElementReader Element(XName name) {
XElement subElem = elem.Element(name);
return subElem == null ? null : new XElementReader(subElem, targetDepth + 1);
}
public IEnumerable<XElementReader> Elements() {
return elem.Elements().Select(e => new XElementReader(e, targetDepth + 1));
}
public IEnumerable<XElementReader> Elements(XName name) {
return elem.Elements(name).Select(e => new XElementReader(e, targetDepth + 1));
}
public XElement ToXElement() {
return elem;
}
#else
private readonly XmlReader reader;
private bool onlyAttributesWereRead = true;
private XElementReader(XmlReader reader, int depth) {
this.reader = reader;
this.targetDepth = depth;
}
public XElementReader(string path) {
this.reader = XmlReader.Create(path);
}
public XElementReader(TextReader reader) {
this.reader = XmlReader.Create(reader);
}
public XElementReader(XmlReader reader) {
this.reader = reader;
}
public XElementReader(Stream stream) {
this.reader = XmlReader.Create(stream);
}
public void Close() {
if (targetDepth != 0) {
throw new InvalidOperationException("Only the outermost reader should be closed");
}
reader.Close();
}
public XName Name {
get {
if (!onlyAttributesWereRead) {
throw new InvalidOperationException("This operation must be done before retrieving any element");
}
GoToDepth(targetDepth);
return XName.Get(reader.LocalName, reader.NamespaceURI);
}
}
public string Value {
get {
if (!onlyAttributesWereRead) {
throw new InvalidOperationException("This operation must be done before retrieving any element");
}
onlyAttributesWereRead = false;
GoToDepth(targetDepth);
return reader.ReadElementContentAsString();
}
}
public XAttribute Attribute(XName name) {
if (!onlyAttributesWereRead) {
throw new InvalidOperationException("This operation must be done before retrieving any element");
}
GoToDepth(targetDepth);
var attribute = reader.GetAttribute(name.LocalName, name.NamespaceName);
return attribute == null ? null : new XAttribute(name, attribute);
}
public IEnumerable<XAttribute> Attributes() {
if (!onlyAttributesWereRead) {
throw new InvalidOperationException("This operation must be done before retrieving any element");
}
GoToDepth(targetDepth);
try {
for (int i = 0; i < reader.AttributeCount; ++i) {
reader.MoveToAttribute(i);
yield return new XAttribute(reader.Name, reader.Value);
}
} finally {
reader.MoveToElement();
}
}
public XElementReader FirstElement() {
onlyAttributesWereRead = false;
if (!GoToDepth(targetDepth + 1)) {
return null;
} else {
return new XElementReader(reader, targetDepth + 1);
}
}
public XElementReader Element(XName name) {
onlyAttributesWereRead = false;
if (!GoToDepth(targetDepth + 1)) {
return null;
}
if (XName.Get(reader.LocalName, reader.NamespaceURI) == name) {
return new XElementReader(reader, targetDepth + 1);
}
if (reader.ReadToNextSibling(name.LocalName, name.NamespaceName)) {
return new XElementReader(reader, targetDepth + 1);
}
return null;
}
public IEnumerable<XElementReader> Elements() {
onlyAttributesWereRead = false;
do {
if (!GoToDepth(targetDepth + 1)) {
yield break;
}
XElementReader innerReader = new XElementReader(reader, targetDepth + 1);
yield return innerReader;
if (innerReader.onlyAttributesWereRead) {
reader.Skip();
}
} while (reader.Depth >= targetDepth + 1);
}
public IEnumerable<XElementReader> Elements(XName name) {
onlyAttributesWereRead = false;
do {
if (!GoToDepth(targetDepth + 1)) {
yield break;
}
if (XName.Get(reader.LocalName, reader.NamespaceURI) == name) {
XElementReader innerReader = new XElementReader(reader, targetDepth + 1);
yield return innerReader;
if (innerReader.onlyAttributesWereRead) {
reader.Skip();
}
} else {
if (!reader.ReadToNextSibling(name.LocalName, name.NamespaceName)) {
yield break;
}
}
} while (reader.Depth >= targetDepth + 1);
}
public XElement ToXElement() {
if (!onlyAttributesWereRead) {
throw new InvalidOperationException("This operation must be done before retrieving any element");
}
onlyAttributesWereRead = false;
GoToDepth(targetDepth);
return (XElement)XNode.ReadFrom(reader); //XElement.Load won't work here if it's not the top level element
}
private bool GoToDepth(int targetDepth) {
int startDepth = reader.Depth;
if (reader.Depth > targetDepth) { //we need to go up
while (reader.NodeType != XmlNodeType.Element || reader.Depth > targetDepth) {
if (reader.NodeType == XmlNodeType.Element) {
reader.Skip();
} else {
if (!reader.Read()) {
return false;
}
if (reader.Depth > startDepth) {
return false;
}
}
}
} else { //we need to go down (or we're already there)
while (reader.NodeType != XmlNodeType.Element || reader.Depth < targetDepth) {
if (reader.NodeType == XmlNodeType.Element && reader.IsEmptyElement) {
reader.Skip();
return false;
} else {
if (!reader.Read()) {
return false;
}
if (reader.Depth < startDepth || reader.NodeType == XmlNodeType.EndElement) {
return false;
}
}
}
}
return reader.Depth == targetDepth;
}
#endif
void IDisposable.Dispose() {
Close();
}
}