73

I was looking for a generic method in .Net to encode a string for use in an Xml element or attribute, and was surprised when I didn't immediately find one. So, before I go too much further, could I just be missing the built-in function?

Assuming for a moment that it really doesn't exist, I'm putting together my own generic EncodeForXml(string data) method, and I'm thinking about the best way to do this.

The data I'm using that prompted this whole thing could contain bad characters like &, <, ", etc. It could also contains on occasion the properly escaped entities: &amp;, &lt;, and &quot;, which means just using a CDATA section may not be the best idea. That seems kinda klunky anyay; I'd much rather end up with a nice string value that can be used directly in the xml.

I've used a regular expression in the past to just catch bad ampersands, and I'm thinking of using it to catch them in this case as well as the first step, and then doing a simple replace for other characters.

So, could this be optimized further without making it too complex, and is there anything I'm missing? :

Function EncodeForXml(ByVal data As String) As String Static badAmpersand As new Regex("&(?![a-zA-Z]{2,6};|#[0-9]{2,4};)") data = badAmpersand.Replace(data, "&amp;") return data.Replace("<", "&lt;").Replace("""", "&quot;").Replace(">", "gt;") End Function 

Sorry for all you C# -only folks-- I don't really care which language I use, but I wanted to make the Regex static and you can't do that in C# without declaring it outside the method, so this will be VB.Net

Finally, we're still on .Net 2.0 where I work, but if someone could take the final product and turn it into an extension method for the string class, that'd be pretty cool too.

Update The first few responses indicate that .Net does indeed have built-in ways of doing this. But now that I've started, I kind of want to finish my EncodeForXml() method just for the fun of it, so I'm still looking for ideas for improvement. Notably: a more complete list of characters that should be encoded as entities (perhaps stored in a list/map), and something that gets better performance than doing a .Replace() on immutable strings in serial.

13 Answers 13

80

Depending on how much you know about the input, you may have to take into account that not all Unicode characters are valid XML characters.

Both Server.HtmlEncode and System.Security.SecurityElement.Escape seem to ignore illegal XML characters, while System.XML.XmlWriter.WriteString throws an ArgumentException when it encounters illegal characters (unless you disable that check in which case it ignores them). An overview of library functions is available here.

Edit 2011/8/14: seeing that at least a few people have consulted this answer in the last couple years, I decided to completely rewrite the original code, which had numerous issues, including horribly mishandling UTF-16.

using System; using System.Collections.Generic; using System.IO; using System.Linq; /// <summary> /// Encodes data so that it can be safely embedded as text in XML documents. /// </summary> public class XmlTextEncoder : TextReader { public static string Encode(string s) { using (var stream = new StringReader(s)) using (var encoder = new XmlTextEncoder(stream)) { return encoder.ReadToEnd(); } } /// <param name="source">The data to be encoded in UTF-16 format.</param> /// <param name="filterIllegalChars">It is illegal to encode certain /// characters in XML. If true, silently omit these characters from the /// output; if false, throw an error when encountered.</param> public XmlTextEncoder(TextReader source, bool filterIllegalChars=true) { _source = source; _filterIllegalChars = filterIllegalChars; } readonly Queue<char> _buf = new Queue<char>(); readonly bool _filterIllegalChars; readonly TextReader _source; public override int Peek() { PopulateBuffer(); if (_buf.Count == 0) return -1; return _buf.Peek(); } public override int Read() { PopulateBuffer(); if (_buf.Count == 0) return -1; return _buf.Dequeue(); } void PopulateBuffer() { const int endSentinel = -1; while (_buf.Count == 0 && _source.Peek() != endSentinel) { // Strings in .NET are assumed to be UTF-16 encoded [1]. var c = (char) _source.Read(); if (Entities.ContainsKey(c)) { // Encode all entities defined in the XML spec [2]. foreach (var i in Entities[c]) _buf.Enqueue(i); } else if (!(0x0 <= c && c <= 0x8) && !new[] { 0xB, 0xC }.Contains(c) && !(0xE <= c && c <= 0x1F) && !(0x7F <= c && c <= 0x84) && !(0x86 <= c && c <= 0x9F) && !(0xD800 <= c && c <= 0xDFFF) && !new[] { 0xFFFE, 0xFFFF }.Contains(c)) { // Allow if the Unicode codepoint is legal in XML [3]. _buf.Enqueue(c); } else if (char.IsHighSurrogate(c) && _source.Peek() != endSentinel && char.IsLowSurrogate((char) _source.Peek())) { // Allow well-formed surrogate pairs [1]. _buf.Enqueue(c); _buf.Enqueue((char) _source.Read()); } else if (!_filterIllegalChars) { // Note that we cannot encode illegal characters as entity // references due to the "Legal Character" constraint of // XML [4]. Nor are they allowed in CDATA sections [5]. throw new ArgumentException( String.Format("Illegal character: '{0:X}'", (int) c)); } } } static readonly Dictionary<char,string> Entities = new Dictionary<char,string> { { '"', "&quot;" }, { '&', "&amp;"}, { '\'', "&apos;" }, { '<', "&lt;" }, { '>', "&gt;" }, }; // References: // [1] http://en.wikipedia.org/wiki/UTF-16/UCS-2 // [2] http://www.w3.org/TR/xml11/#sec-predefined-ent // [3] http://www.w3.org/TR/xml11/#charsets // [4] http://www.w3.org/TR/xml11/#sec-references // [5] http://www.w3.org/TR/xml11/#sec-cdata-sect } 

Unit tests and full code can be found here.

Sign up to request clarification or add additional context in comments.

10 Comments

Good answer, have seen the similar solution from this article: seattlesoftware.wordpress.com/2008/09/11/…
That article explains the problem really well.
For the bit (0x100000 <= c && c <= 0x10FFFF) my compiler warns me: "Comparison to integral constant is useless; the constant is outside the range of type 'char'"
Thanks codeulike — pointing out the warning was the kick I needed to finally rewrite the original, buggy code. =) Please try the new code if you get a chance.
+1 for updating your code :) and revisiting the question (helped me out)
|
35

SecurityElement.Escape

documented here

2 Comments

This seems like what I'm looking for, but there are some comments at the bottom indicating the implementation is less than stellar.
link dead
27

In the past I have used HttpUtility.HtmlEncode to encode text for xml. It performs the same task, really. I haven't run into any issues with it yet, but that's not to say I won't in the future. As the name implies, it was made for HTML, not XML.

You've probably already read it, but here is an article on xml encoding and decoding.

EDIT: Of course, if you use an xmlwriter or one of the new XElement classes, this encoding is done for you. In fact, you could just take the text, place it in a new XElement instance, then return the string (.tostring) version of the element. I've heard that SecurityElement.Escape will perform the same task as your utility method as well, but havent read much about it or used it.

EDIT2: Disregard my comment about XElement, since you're still on 2.0

1 Comment

Note that neither System.Xml.Linq.XText instances nor the System.SecuritySecurityElement.Escape() nor the (made for HTML) System.Web.HttpUtility.HtmlEncode() methods handle translating illegal chars. into character references (e.g, &#x1B; for ESC). By contrast, System.Xml.XmlDocument instances do.
13

Microsoft's AntiXss library AntiXssEncoder Class in System.Web.dll has methods for this:

AntiXss.XmlEncode(string s) AntiXss.XmlAttributeEncode(string s) 

it has HTML as well:

AntiXss.HtmlEncode(string s) AntiXss.HtmlAttributeEncode(string s) 

Comments

11

In .net 3.5+

new XText("I <want> to & encode this for XML").ToString(); 

Gives you:

I &lt;want&gt; to &amp; encode this for XML

Turns out that this method doesn't encode some things that it should (like quotes).

SecurityElement.Escape (workmad3's answer) seems to do a better job with this and it's included in earlier versions of .net.

If you don't mind 3rd party code and want to ensure no illegal characters make it into your XML, I would recommend Michael Kropat's answer.

3 Comments

&amp; isn't valid XML. I would assume it would use the XML entity: &#38;
It seems that the easiest solution is the best sometimes. Saved me a large chunk of time, mucho appreciated.
@Armstrongest, &amp; is valid XML - see en.wikipedia.org/wiki/…. Ronnie: System.Xml.Linq.XText correctly does not escape " and ', because XML doesn't require it. However, like SecurityElement.Escape it also doesn't handle translating illegal chars. into character references. By contrast, System.Xml.XmlDocument does.
5

XmlTextWriter.WriteString() does the escaping.

1 Comment

Or, use it's relative on XmlNode object - .InnerText Getter and Setter decode and encode.
4

System.XML handles the encoding for you, so you don't need a method like this.

17 Comments

Or go shout at the guys who aren't encoding their xml correctly.
@Sekhat That's an unreasonable solution. In the real world, large data vendors often cannot be bothered to fix these types of issues, as doing so would break their clients' data.
@TrevorSullivan That approach works reasonably well in academia, but not so much elsewhere. If you only knew how half-baked some of the financial world's implementations of common specs are (ranging from CRC implementations to things as trivial as XML - I'm speaking from my first hand experience only), you might decide to keep your money in a mattress at home.
@Mick: if you knew how mattresses were made today, you might decide to take your money back to the bank.
This was accepted? It's not an answer. Sometimes we have to work with code that is using XML strings
|
3

If this is an ASP.NET app why not use Server.HtmlEncode() ?

5 Comments

This is in a library that will be used for both asp.net apps and batch processing (desktop).
You can actually access Server.HTMLEncode() in a desktop app - all you have to do is ad a reference to System.Web
Neither Server.HtmlEncode() nor HttpUtility.HtmlAttributeEncode() replace characters like '\0'
Just noting for anyone thinking this is a good idea, System.Web is a big overhead and not really meant for class libraries/windows apps
@stuartdotnet - hence the caveat "If this is an ASP.NET app".
3

This might be the case where you could benefit from using the WriteCData method.

public override void WriteCData(string text) Member of System.Xml.XmlTextWriter Summary: Writes out a <![CDATA[...]]> block containing the specified text. Parameters: text: Text to place inside the CDATA block. 

A simple example would look like the following:

writer.WriteStartElement("name"); writer.WriteCData("<unsafe characters>"); writer.WriteFullEndElement(); 

The result looks like:

<name><![CDATA[<unsafe characters>]]></name> 

When reading the node values the XMLReader automatically strips out the CData part of the innertext so you don't have to worry about it. The only catch is that you have to store the data as an innerText value to an XML node. In other words, you can't insert CData content into an attribute value.

Comments

3

If you're serious about handling all of the invalid characters (not just the few "html" ones), and you have access to System.Xml, here's the simplest way to do proper Xml encoding of value data:

string theTextToEscape = "Something \x1d else \x1D <script>alert('123');</script>"; var x = new XmlDocument(); x.LoadXml("<r/>"); // simple, empty root element x.DocumentElement.InnerText = theTextToEscape; // put in raw string string escapedText = x.DocumentElement.InnerXml; // Returns: Something &#x1D; else &#x1D; &lt;script&gt;alert('123');&lt;/script&gt; // Repeat the last 2 lines to escape additional strings. 

It's important to know that XmlConvert.EncodeName() is not appropriate, because that's for entity/tag names, not values. Using that would be like Url-encoding when you needed to Html-encode.

Comments

0

Brilliant! That's all I can say.

Here is a VB variant of the updated code (not in a class, just a function) that will clean up and also sanitize the xml

Function cXML(ByVal _buf As String) As String Dim textOut As New StringBuilder Dim c As Char If _buf.Trim Is Nothing OrElse _buf = String.Empty Then Return String.Empty For i As Integer = 0 To _buf.Length - 1 c = _buf(i) If Entities.ContainsKey(c) Then textOut.Append(Entities.Item(c)) ElseIf (AscW(c) = &H9 OrElse AscW(c) = &HA OrElse AscW(c) = &HD) OrElse ((AscW(c) >= &H20) AndAlso (AscW(c) <= &HD7FF)) _ OrElse ((AscW(c) >= &HE000) AndAlso (AscW(c) <= &HFFFD)) OrElse ((AscW(c) >= &H10000) AndAlso (AscW(c) <= &H10FFFF)) Then textOut.Append(c) End If Next Return textOut.ToString End Function Shared ReadOnly Entities As New Dictionary(Of Char, String)() From {{""""c, "&quot;"}, {"&"c, "&amp;"}, {"'"c, "&apos;"}, {"<"c, "&lt;"}, {">"c, "&gt;"}} 

Comments

0

You can use the built-in class XAttribute, which handles the encoding automatically:

using System.Xml.Linq; XDocument doc = new XDocument(); List<XAttribute> attributes = new List<XAttribute>(); attributes.Add(new XAttribute("key1", "val1&val11")); attributes.Add(new XAttribute("key2", "val2")); XElement elem = new XElement("test", attributes.ToArray()); doc.Add(elem); string xmlStr = doc.ToString(); 

Comments

0

Here is a single line solution using the XElements. I use it in a very small tool. I don't need it a second time so I keep it this way. (Its dirdy doug)

StrVal = (<x a=<%= StrVal %>>END</x>).ToString().Replace("<x a=""", "").Replace(">END</x>", "") 

Oh and it only works in VB not in C#

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.