XML
SharpProspero.Xml reads and writes XML for a configuration or data file without pulling in a system module. It gives you both a small tree model for whole documents and a forward-only reader and writer for streaming, and it never loads an external DTD or entity, so it stays safe on a constrained target.
Parsing is checked for well-formedness — matching tags, one root element — and resolves the standard entity references (
<,>,&,",', and numeric&#nn;or&#xNN;; a numeric reference outside Unicode or in the surrogate range is rejected). It refuses documents nested deeper than 256 levels so an adversarial file cannot overflow the stack. A<!DOCTYPE>is skipped rather than fetched, and an entity the parser does not recognize is reported as an error instead of looked up.
The document model
For most configuration and level files you want the whole document in memory as a tree. Three types make that up: XmlDocument holds the single root; XmlElement is a node with a name, attributes, direct text, and child elements; and XmlAttribute is a name/value pair.
XmlDocument.Parse turns text into a tree and throws XmlException if the markup is malformed. Query the tree with Attribute, Element, Elements, and Descendants:
using SharpProspero.Xml;
using SharpProspero.Storage;
XmlDocument doc = XmlDocument.Parse(PackageFile.ReadAllText("/app0/level.xml"));
string levelName = doc.Root.AttributeOrDefault("name", "untitled");
foreach (XmlElement enemy in doc.Root.Element("enemies")!.Elements("enemy"))
{
string type = enemy.AttributeOrDefault("type", "grunt");
int x = int.Parse(enemy.AttributeOrDefault("x", "0"));
// place the enemy at x...
}
Attribute returns the value or null; AttributeOrDefault returns a fallback instead. Element returns the first matching child (or null), Elements enumerates the direct children with a given name, and Descendants walks the whole subtree for a name at any depth. Children lists the direct child elements in document order and Attributes lists every attribute, for walking a tree whose names you do not know in advance. Text is the element’s own text with entities resolved, and it is settable, so the same property reads a document and builds one.
Building a tree
You build a document the same way you read one. SetAttribute and Add return the element they were called on, so calls chain; AddElement creates a child and returns the new child so you can fill it in:
using SharpProspero.Xml;
var save = new XmlElement("save").SetAttribute("slot", "1");
save.AddElement("score").Text = "1200";
string xml = new XmlDocument(save).ToXml(indent: true);
ToXml writes the tree back to text, indented when you ask, with an XML declaration by default (ToXml(indent: true, declaration: false) drops the declaration). XmlDocument.ToString is the indented form.
Streaming with XmlReader
When a document is large, or you only need a few values from it, skip the tree and pull nodes one at a time. XmlReader is a forward-only pull parser: call Read to advance and inspect NodeType, Name, Value, Attributes, and IsEmptyElement.
using SharpProspero.Xml;
var reader = new XmlReader(text);
while (reader.Read())
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
// reader.Name is the tag; reader.Attributes holds its attributes.
// A self-closing <tag/> also sets reader.IsEmptyElement and emits no end tag.
break;
case XmlNodeType.Text:
// reader.Value is the character data, with entities resolved.
break;
case XmlNodeType.EndElement:
break;
}
}
Read returns false at the end of the document. Each XmlAttribute in Attributes exposes a Name and an unescaped Value. NodeType is one of:
XmlNodeType |
Node |
|---|---|
None |
Before the first read, or after the end |
XmlDeclaration |
The <?xml ... ?> declaration |
Element |
A start tag (self-closing tags also set IsEmptyElement) |
EndElement |
An end tag |
Text |
Character data between tags |
CData |
A <![CDATA[ ... ]]> section |
Comment |
A <!-- ... --> comment |
ProcessingInstruction |
A <? ... ?> instruction |
Whitespace |
Character data that is entirely whitespace |
XmlDocument.Parse is itself built on this reader, so both paths agree on what counts as well-formed.
Writing with XmlWriter
XmlWriter builds output directly, without a tree. It escapes text and attribute values, checks that names are legal, and self-closes an element that gets no content. Elements open and close in pairs, and attributes must be written right after WriteStartElement, before any content or child.
using SharpProspero.Xml;
var writer = new XmlWriter(indent: true);
writer.WriteDeclaration();
writer.WriteStartElement("save");
writer.WriteAttribute("slot", "1");
writer.WriteElementString("score", "1200");
writer.WriteEndElement();
string xml = writer.ToString();
WriteElementString is the shorthand for an open/content/close triple. WriteString writes escaped text into the current element, WriteCData writes a verbatim CDATA section, and WriteComment writes a comment. ToString closes any elements still open and returns the text. Every write method returns the writer, so a whole document can be produced as one chained expression.
WriteAttributeonly works while a start tag is still open. Once you write content or a child element the start tag closes, and a laterWriteAttributethrowsInvalidOperationException. A name that contains whitespace or markup characters is rejected up front rather than written into output that cannot be read back. A control character other than tab, carriage return or line feed is refused in text and in an attribute value — a document cannot carry one at all, escaped or not — soWriteString,WriteAttributeandWriteElementStringraiseArgumentExceptionnaming the character rather than produce output no reader loads.
Handling malformed input
Any parse failure — a mismatched end tag, an unterminated attribute, an unknown entity, a second root — surfaces as an XmlException that carries the exact position. Catch it and read Line and Column:
using SharpProspero.Xml;
using SharpProspero.Diagnostics;
try
{
XmlDocument doc = XmlDocument.Parse(text);
// use doc...
}
catch (XmlException ex)
{
Log.Warning($"Bad XML at line {ex.Line}, column {ex.Column}: {ex.Message}");
}
Both Line and Column are 1-based, and the message already includes the position, so it reads well in a log on its own.
For a lighter-weight configuration format, see the JSON, INI, and CSV readers on Files and storage.