Asked 1 month ago by AuroraProbe078
Why does XDocument throw an exception for XML with invalid hexadecimal characters while XmlDocument does not?
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Asked 1 month ago by AuroraProbe078
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
We are receiving XML documents that contain hex entity expressions for control characters. These documents are likely generated using XmlDocument, which encodes invalid characters (such as control code U+001E in the example) into hexadecimal character entities. However, when processing the same XML with XDocument.Parse(), an exception is thrown because XDocument enforces the XML 1.0 specification more strictly.
Example:
CSHARPstatic void Main(string[] args) { string message = "Hello, \x1EWorld!"; // string with control code 1E encoded. // This block completes - create an xml document incorporating the message string XmlDocument xmlDoc = new XmlDocument(); XmlElement root = xmlDoc.CreateElement("greeting"); xmlDoc.AppendChild(root); root.InnerText = message; Console.WriteLine(xmlDoc.OuterXml); // Outputs: <greeting>Hello, World!</greeting> // This block fails - XDocument creation of document containing control-code character x1E try { XDocument xdoc = new XDocument( new XElement("greeting", message) ); Console.WriteLine(xdoc.ToString()); } catch (Exception ex) { Console.WriteLine($"XDocument parsing error: {ex.Message}"); } }
This behavior highlights the difference between XmlDocument and XDocument. XmlDocument converts invalid characters to numeric entities, while XDocument strictly validates the content against the XML 1.0 rules, leading to exceptions when encountering disallowed characters.
What are the recommended workarounds or alternative approaches to handle such XML content when using XDocument?
Comments 0
•Answers 2
•Views 89
Answer by TychoAI Agent • 1 month ago
When you use XmlDocument to create or parse XML, it accepts control-code characters by encoding them as numeric character entities (for example, �). However, when you later pass that document to XDocument (or use LINQ to XML), the parser enforces the XML 1.0 specification more strictly. In XML 1.0 the vast majority of control characters (with the exception of tab, newline, and carriage return) are not allowed—even as escaped entities. As a result, when XDocument parses content that represents a disallowed Unicode code point (like U+001E), it throws an exception.
Below is an example that illustrates the situation:
CSHARPusing System; using System.Xml; using System.Xml.Linq; class Program { static void Main(string[] args) { // Create a string that includes an illegal control character (U+001E) string message = "Hello, \x1EWorld!"; // Using XmlDocument to produce XML content: XmlDocument xmlDoc = new XmlDocument(); XmlElement root = xmlDoc.CreateElement("greeting"); xmlDoc.AppendChild(root); root.InnerText = message; // The control char is encoded as  Console.WriteLine(xmlDoc.OuterXml); // Output: <greeting>Hello, World!</greeting> // Using XDocument to create XML: try { XDocument xdoc = new XDocument( new XElement("greeting", message) ); Console.WriteLine(xdoc.ToString()); } catch (Exception ex) { Console.WriteLine($"XDocument parsing error: {ex.Message}"); } } }
In this example, although XmlDocument converts the invalid control character into a hex entity, XDocument does not allow the control character in the content at all—even if it were represented as an entity in the XML text. Because XML 1.0 disallows most control characters, LINQ to XML’s more rigorous validation fails when it encounters them.
These steps can help ensure compatibility when moving between XmlDocument and XDocument.
Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?
No comments yet.
Answer by MartianRanger392 • 1 month ago
The difference here is that LINQ to XML strictly enforces the Character Range constraint of the Extensible Markup Language (XML) 1.0 (Fourth Edition), while the older XmlDocument
apparently does not:
Character Range
XML[2] Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] /* any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. */
As you can see, #x1E
is not in the above range, meaning an XML document that contains this character is not, strictly speaking, well-formed according to the version of the XML standard supported by .NET.[1].
If you don't want this, you could create versions of XDocument.Parse()
and XDocument.ToString()
that set XmlReaderSettings.CheckCharacters
and XmlWriterSettings.CheckCharacters
to false
:
CSHARPpublic static class XDocumentExtensions { static readonly XmlReaderSettings noCheckedCharacterParseSettings = new() { CheckCharacters = false, }; static readonly XmlReaderSettings checkedCharacterParseSettings = new() { CheckCharacters = true, }; public static XDocument Parse(string xml, bool checkCharacters) => Parse(xml, checkCharacters ? checkedCharacterParseSettings : noCheckedCharacterParseSettings); public static XDocument Parse(string xml, XmlReaderSettings settings) { using var reader = new StringReader(xml); using var xmlReader = XmlReader.Create(reader, settings); return XDocument.Load(xmlReader); } static readonly XmlWriterSettings noCheckedCharacterToStringSettings = new() { CheckCharacters = false, Indent = true, OmitXmlDeclaration = true, }; static readonly XmlWriterSettings checkedCharacterToStringSettings = new() { CheckCharacters = true, Indent = true, OmitXmlDeclaration = true, }; public static string ToString(this XNode node, bool checkCharacters) => node.ToString(checkCharacters ? checkedCharacterToStringSettings : noCheckedCharacterToStringSettings); public static string ToString(this XNode node, XmlWriterSettings settings) { using var writer = new StringWriter(); using (var xmlWriter = XmlWriter.Create(writer, settings)) { node.WriteTo(xmlWriter); } return writer.ToString(); } }
Then modify your code as follows:
CSHARPXDocument xdoc = new XDocument( new XElement("greeting", message) ); Console.WriteLine(xdoc.ToString(checkCharacters : false));
And
CSHARPXDocument xDoc = XDocumentExtensions.Parse(xmlWithEscapedHexEntity, checkCharacters : false); Console.WriteLine(xDoc.ToString(checkCharacters : false));
And you will be able to parse and format XML that is malformed purely due to containing invalid XML characters without any exceptions being thrown. Demo fiddle here.
That being said, I don't really recommend doing this, as the XML you generate will not be accepted by any receiving system that requires strict conformance with the XML standard. If you would prefer to remove invalid characters from your XML text, see:
Now, as for why this difference exists? Hard to say for sure, but a check of the reference source shows that XmlDocument
uses XmlTextReader
to parse its XML. Both these types are very old (dating to .NET 1.1), and XmlTextReader
was deprecated in .NET 2.0:
Starting with the .NET Framework 2.0, we recommend that you use the XmlReader class instead.
My guess is that Microsoft simply didn't implement character range checking in the initial .NET 1.1 XmlTextReader
and XmlTextWriter
implementations, then later did so when they introduced XmlReader
and XmlWriter
in .NET 2, and LINQ to XML in .NET 3.5. And, while a fair amount of guidance about early .NET versions was lost when MSDN links were retired, I did find the MSDN page Creating XML Readers, archived in 2013 but no longer actively available, that alludes to incomplete conformance checking by XmlTextReader
:
By using the Create method and the XmlReaderSettings class you get the following benefits:
- Take full advantage of all the new features added to the XmlReader class in the .NET Framework 2.0 release. There are certain features, such as better conformance checking and compliance to the XML 1.0 recommendation, that are available only on XmlReader objects created by the Create method.
So it may be that guidance from Microsoft that character conformance checking was not fully implemented in the .NET 1.1 existed 10 or 15 years ago, but has since been lost.
Update
If you need a Parse()
function that strips invalid XML characters whether embedded directly in the XML text or hex-encoded as character entities, you could use the following extension methods:
CSHARPpublic static class XDocumentExtensions { static readonly XmlReaderSettings noCheckedCharacterParseSettings = new() { CheckCharacters = false, }; public static XDocument ParseAndRemoveInvalidXmlCharacters(string xml, char? fallback = null) { ArgumentNullException.ThrowIfNull(xml); // From testing it seems that CheckCharacters=false only allows invalid character entities whose value falls outside the range from the standard // https://www.w3.org/TR/2006/REC-xml-20060816/#NT-Char // [2] Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] /* any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. */ // Invalid characters directly embedded in the character stream must be stripped out manually. using var reader = new StringReader(xml.RemoveInvalidXmlCharacters(fallback)); using var xmlReader = XmlReader.Create(reader, noCheckedCharacterParseSettings); return XDocument.Load(xmlReader).RemoveInvalidXmlCharacters(fallback); } public static TXObject RemoveInvalidXmlCharacters<TXObject>(this TXObject node, char? fallback = null) where TXObject : XObject { switch (node) { case XText text: text.Value = text.Value.RemoveInvalidXmlCharacters(fallback); break; case XAttribute attribute: attribute.Value = attribute.Value.RemoveInvalidXmlCharacters(fallback); break; case XComment comment: comment.Value = comment.Value.RemoveInvalidXmlCharacters(); break; case XDocument doc: doc.Root?.RemoveInvalidXmlCharacters(); break; case XElement element: foreach (var attr in element.Attributes()) attr.RemoveInvalidXmlCharacters(); foreach (var child in element.Nodes()) child.RemoveInvalidXmlCharacters(); break; case XContainer container: // XDocument foreach (var child in container.Nodes()) child.RemoveInvalidXmlCharacters(); break; // Not done: XDocumentType, XProcessingInstruction } return node; } public static string RemoveInvalidXmlCharacters(this string xmlText, char? fallback = null) { ArgumentNullException.ThrowIfNull(xmlText); StringBuilder? sb = null; for (int i = 0; i < xmlText.Length; i++) { if (XmlConvert.IsXmlChar(xmlText[i])) { if (sb != null) sb.Append(xmlText[i]); } else if (i < xmlText.Length - 1 && XmlConvert.IsXmlSurrogatePair(xmlText[i+1], xmlText[i])) // Yes this order is correct. { if (sb != null) sb.Append(xmlText, i, 2); i++; } else { if (sb == null) { sb = new(); sb.Append(xmlText, 0, i); } if (fallback != null) sb.Append(fallback.Value); } } return sb?.ToString() ?? xmlText; } }
And then do:
CSHARPvar xdoc = new XDocument( new XElement("greeting", message) ).RemoveInvalidXmlCharacters();
Or
CSHARPvar xDoc = XDocumentExtensions.ParseAndRemoveInvalidXmlCharacters(xmlWithInvalidCharacters);
Demo fiddle #2 here.
[1] While .NET officially only supports the XML 1.0 (4th Edition) standard, the 5th Edition has a similar constraint:
XML[2] Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] /* any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. */
Now, as noted by Michael Kay in comments, escape characters such as 
are allowed by XML 1.1, however .NET never implemented support for this XML version.
No comments yet.
No comments yet.