The XmlReader was a significant innovation in XML parsing due to its pull model approach and caused a significant amount of acclaim in the industry. Until then developers had to endure the SAX push model, which although great for XML parser implementers (just push out an event when something happens), was painful for developers to use. The XmlReader reversed this position allowing you to do simple, procedural code where you could get the XmlReader to do the heavy lifting for you, like skipping elements that you did not care about or throwing away white space from the document. The XmlReader was inspired by seeing the use of other readers in the .NET Framework such as the StringReader class and applying the same principles to XML.
There are aspects of the XmlReader design where we had to choose between usability and performance. For example, due to the attribute indexer methods all the attributes for an element have to be cached, which does not allow for a complete streaming API. On the whole the majority of XML documents have small numbers of attributes so improved usability was the best design.
In addition to the reason Mark just noted, you have to cache all the attributes anyway in order to know the namespace of the element. SAX had the exact same problem—only worse. In SAX, an Array object is created with all of the attributes that is then passed to your handler. At least here we avoid that array creation.
The Read and Skip methods are, of course, different beasts. But they are more similar than you might think. If the reader is on any node besides a non-empty element node, Read and Skip behave the same. Otherwise, Skip positions the reader following the corresponding EndElement node, not exposing any properties along the way (while Read will expose the properties).
In general the XmlReader makes recursive descent parsing of a given XML document a snap. In fact, it is so easy that the XmlSerializer generates IL code for you that calls the XmlReader to parse the XML while it builds your own custom objects from what it finds in the stream. Hence, XmlSerialization is probably the #1 customer of the XmlReader class in terms of overall volume of XML parsed.
Notice that XmlReader is abstract. If you want to just parse XML text, you need the concrete subclass XmlTextReader discussed later. Why did we bother with an abstract base class? Because we envisioned the creation of lots of different kinds of XML readers that took their data from other sources besides text. It also means you can plug in additional behavior into the XML processing pipeline. Suppose someone consumes an XML reader. Well, you can wrap the XmlTextReader with an XmlValidatingReader and pass that instead, and now you are causing your consumer to validate while they parse without them even knowing it.
Although the XmlReader was designed as an abstract class, it has a few properties that are bound to the text representation of XML, such as QuoteChar or IsEmptyElement. As was pointed out quite a few times by our users, it is annoying that the XmlReader reports empty and non-empty elements differently. For non-empty element <a>…</a> the reader returns Element and EndElement events, but for empty element <a/> it only returns a single Element event. The user has to check the IsEmptyElement property to see whether he should expect a closing EndElement or not. In many cases this causes two similar code paths in the handling code, one for empty elements and one for non-empty ones.