XDocument
(and more generally XmlReader
) will load XML without converting >
characters to >
(In fact just the opposite happens -- >
will be unescaped to >
by XmlReader
). You may verify that by doing:
var xmlString = @"<?xml version=""1.0"" encoding=""utf-8""?><Foo Name=""a->b""></Foo>";
var doc = XDocument.Parse(xmlString);
Assert.AreEqual("a->b", doc.Root.Attribute("Name").Value); // Passes successfully
Demo fiddle #1 here.
Instead what you are seeing is that, when writing your XDocument
back to XML, XmlWriter
unconditionally escapes >
as >
even when not strictly necessary. (An XmlWriter
is always used to format an XNode
to XML, either explicitly when you construct it yourself to write to some Stream
or TextWriter
, or internally by XNode.ToString()
.)
If you don't want this, you will have to subclass XmlWriter
and modify the logic of XmlWriter.WriteString(String)
to use your preferred escaping. However XmlWriter
itself is abstract; the XmlWriter
returned by XmlWriter.Create()
is some internal concrete subclass which cannot be subclassed directly. Thus you will need to use the decorator pattern to wrap the writer returned by XmlWriter.Create()
:
public class NoEndBracketEscapingXmlWriter : XmlWriterDecorator
{
bool OnlyForAttributes { get; }
public NoEndBracketEscapingXmlWriter(XmlWriter baseWriter) : this(baseWriter, false) { }
public NoEndBracketEscapingXmlWriter(XmlWriter baseWriter, bool onlyForAttributes) : base(baseWriter) => this.OnlyForAttributes = onlyForAttributes;
public override void WriteString(string text)
{
//The right angle bracket (>) may be represented using the string >, and must, for compatibility, be escaped using either > or a character reference when it appears in the string " ]]> " in content, when that string is not marking the end of a CDATA section.
if (WriteState == WriteState.Prolog || (WriteState != WriteState.Attribute && OnlyForAttributes))
{
base.WriteString(text);
return;
}
int prevIndex = 0, index;
char [] buffer = null;
while ((index = text.IndexOf('>', prevIndex)) >= 0)
{
if (buffer == null)
buffer = text.ToCharArray();
if (WriteState != WriteState.Attribute && text.AsSpan().Slice(prevIndex, index - prevIndex).EndsWith("]]")) // Logic correction suggested by Jeroen Mostert https://stackoverflow.com/users/4137916/jeroen-mostert
{
// > appearing in "]]>" must still be escaped
base.WriteChars(buffer, prevIndex, index - prevIndex + 1);
}
else
{
base.WriteChars(buffer, prevIndex, index - prevIndex);
base.WriteRaw(">");
}
prevIndex = index + 1;
}
if (buffer == null)
base.WriteString(text);
else if (prevIndex < buffer.Length)
base.WriteChars(buffer, prevIndex, buffer.Length - prevIndex);
}
}
public class XmlWriterDecorator : XmlWriter
{
// Taken from this answer https://stackoverflow.com/a/32150990/3744182
// by https://stackoverflow.com/users/3744182/dbc
// To https://stackoverflow.com/questions/32149676/custom-xmlwriter-to-skip-a-certain-element
// NOTE: async methods not implemented
readonly XmlWriter baseWriter;
public XmlWriterDecorator(XmlWriter baseWriter) => this.baseWriter = baseWriter ?? throw new ArgumentNullException();
protected virtual bool IsSuspended { get { return false; } }
public override WriteState WriteState => baseWriter.WriteState;
public override XmlWriterSettings Settings => baseWriter.Settings;
public override XmlSpace XmlSpace => baseWriter.XmlSpace;
public override string XmlLang => baseWriter.XmlLang;
public override void Close() => baseWriter.Close();
public override void Flush() => baseWriter.Flush();
public override string LookupPrefix(string ns) => baseWriter.LookupPrefix(ns);
public override void WriteBase64(byte[] buffer, int index, int count)
{
if (IsSuspended)
return;
baseWriter.WriteBase64(buffer, index, count);
}
public override void WriteCData(string text)
{
if (IsSuspended)
return;
baseWriter.WriteCData(text);
}
public override void WriteCharEntity(char ch)
{
if (IsSuspended)
return;
baseWriter.WriteCharEntity(ch);
}
public override void WriteChars(char[] buffer, int index, int count)
{
if (IsSuspended)
return;
baseWriter.WriteChars(buffer, index, count);
}
public override void WriteComment(string text)
{
if (IsSuspended)
return;
baseWriter.WriteComment(text);
}
public override void WriteDocType(string name, string pubid, string sysid, string subset)
{
if (IsSuspended)
return;
baseWriter.WriteDocType(name, pubid, sysid, subset);
}
public override void WriteEndAttribute()
{
if (IsSuspended)
return;
baseWriter.WriteEndAttribute();
}
public override void WriteEndDocument()
{
if (IsSuspended)
return;
baseWriter.WriteEndDocument();
}
public override void WriteEndElement()
{
if (IsSuspended)
return;
baseWriter.WriteEndElement();
}
public override void WriteEntityRef(string name)
{
if (IsSuspended)
return;
baseWriter.WriteEntityRef(name);
}
public override void WriteFullEndElement()
{
if (IsSuspended)
return;
baseWriter.WriteFullEndElement();
}
public override void WriteProcessingInstruction(string name, string text)
{
if (IsSuspended)
return;
baseWriter.WriteProcessingInstruction(name, text);
}
public override void WriteRaw(string data)
{
if (IsSuspended)
return;
baseWriter.WriteRaw(data);
}
public override void WriteRaw(char[] buffer, int index, int count)
{
if (IsSuspended)
return;
baseWriter.WriteRaw(buffer, index, count);
}
public override void WriteStartAttribute(string prefix, string localName, string ns)
{
if (IsSuspended)
return;
baseWriter.WriteStartAttribute(prefix, localName, ns);
}
public override void WriteStartDocument(bool standalone) => baseWriter.WriteStartDocument(standalone);
public override void WriteStartDocument() => baseWriter.WriteStartDocument();
public override void WriteStartElement(string prefix, string localName, string ns)
{
if (IsSuspended)
return;
baseWriter.WriteStartElement(prefix, localName, ns);
}
public override void WriteString(string text)
{
if (IsSuspended)
return;
baseWriter.WriteString(text);
}
public override void WriteSurrogateCharEntity(char lowChar, char highChar)
{
if (IsSuspended)
return;
baseWriter.WriteSurrogateCharEntity(lowChar, highChar);
}
public override void WriteWhitespace(string ws)
{
if (IsSuspended)
return;
baseWriter.WriteWhitespace(ws);
}
}
And then you could use it e.g. in the following extension method:
public static class XNodeExtensions
{
public static string ToStringNoEndBracketEscaping(this XNode node)
{
if (node == null)
throw new ArgumentNullException(nameof(node));
using var textWriter = new StringWriter();
using (var innerWriter = XmlWriter.Create(textWriter, new XmlWriterSettings { Indent = true, OmitXmlDeclaration = true }))
using (var writer = new NoEndBracketEscapingXmlWriter(innerWriter))
{
node.WriteTo(writer);
}
return textWriter.ToString();
}
}
And now if you do
var newXml = doc.ToStringNoEndBracketEscaping();
The result will be
<Foo Name="a->b"></Foo>
Demo fiddle #2 here.