How to Serialize a String as Cdata Using Xmlserializer

How do you serialize a string as CDATA using XmlSerializer?

[XmlRoot("root")]
public class Sample1Xml
{
internal Sample1Xml()
{
}

[XmlElement("node")]
public NodeType Node { get; set; }

#region Nested type: NodeType

public class NodeType
{
[XmlAttribute("attr1")]
public string Attr1 { get; set; }

[XmlAttribute("attr2")]
public string Attr2 { get; set; }

[XmlIgnore]
public string Content { get; set; }

[XmlText]
public XmlNode[] CDataContent
{
get
{
var dummy = new XmlDocument();
return new XmlNode[] {dummy.CreateCDataSection(Content)};
}
set
{
if (value == null)
{
Content = null;
return;
}

if (value.Length != 1)
{
throw new InvalidOperationException(
String.Format(
"Invalid array length {0}", value.Length));
}

Content = value[0].Value;
}
}
}

#endregion
}

How do you (de)serialize a list of strings as CDATA using XmlSerializer

Whilst simple proxies work with single values, you have to do a deeper proxying for collections, because of the way the .NET XML serialization machinery works:

[XmlRootAttribute("root")]
public class TestConfig
{
public TestConfig()
{
TestList = new List<string>();
}

private List<string> testList;

[XmlIgnore]
public List<string> TestList
{
get
{
if (this.testList == null)
{
var newCollection = new List<string>();

if (this.cdataList != null)
{
foreach (var x in this.cdataList)
{
newCollection.Add(x.Value);
}
}

this.testList = newCollection;
this.cdataList = null;
}

return this.testList;
}
set
{
this.testList = value;
this.cdataList = null;
}
}

private List<XmlCDataSection> cdataList;

[XmlArray("tlist")]
[XmlArrayItem("item")]
public List<XmlCDataSection> CdataList
{
get
{
if (this.cdataList == null)
{
var newCollection = new List<XmlCDataSection>();

if (this.testList != null)
{
foreach (var x in this.testList)
{
newCollection.Add(new XmlDocument().CreateCDataSection(x));
}
}

this.cdataList = newCollection;
this.testList = null;
}

return this.cdataList;
}
set
{
this.cdataList = value;
this.testList = null;
}
}

public void Save(string path)
{
var serializer = new XmlSerializer(GetType());
using (var stream = new StreamWriter(path))
{
serializer.Serialize(stream, this);
}
}

public static TestConfig Load(string path)
{
var serializer = new XmlSerializer(typeof(TestConfig));
using (var stream = new StreamReader(path))
{
return (TestConfig)serializer.Deserialize(stream);
}
}
}

The problem is that the serialization code doesn't just get and set the collections in one go. For example, when it's deserializing, it either creates a new collection, or gets one that's already set on the property, and adds to it. If you've created a new collection here computed from the "real" collection that your application needs to deal with, then any changes to the computed collection won't be reflected in the "real" collection.

To work around this, what I've done in the code above is to transfer ownership of the collection from the "real" collection to the "proxy" collection, and back again, depending on which collection property is being accessed. The cost of transferring ownership is incurred only when switching from one property to the other, so successive accesses to the "real" TestList collection in your application won't incur that expense.

This is somewhat inelegant though if you have many such collections. If you wanted to have all your element text serialized as CDATA, you could implement a custom XmlWriter, like the following:

/// <summary>
/// Custom XmlWriter.
/// Wraps up another XmlWriter to intercept string writes within
/// elements and writes them as CDATA instead.
/// </summary>
public class XmlCDataWriter : XmlWriter
{
XmlWriter w;

public XmlCDataWriter(XmlWriter baseWriter)
{
this.w = baseWriter;
}

public override void Close()
{
w.Close();
}

public override void Flush()
{
w.Flush();
}

public override string LookupPrefix(string ns)
{
return w.LookupPrefix(ns);
}

public override void WriteBase64(byte[] buffer, int index, int count)
{
w.WriteBase64(buffer, index, count);
}

public override void WriteCData(string text)
{
w.WriteCData(text);
}

public override void WriteCharEntity(char ch)
{
w.WriteCharEntity(ch);
}

public override void WriteChars(char[] buffer, int index, int count)
{
w.WriteChars(buffer, index, count);
}

public override void WriteComment(string text)
{
w.WriteComment(text);
}

public override void WriteDocType(string name, string pubid, string sysid, string subset)
{
w.WriteDocType(name, pubid, sysid, subset);
}

public override void WriteEndAttribute()
{
w.WriteEndAttribute();
}

public override void WriteEndDocument()
{
w.WriteEndDocument();
}

public override void WriteEndElement()
{
w.WriteEndElement();
}

public override void WriteEntityRef(string name)
{
w.WriteEntityRef(name);
}

public override void WriteFullEndElement()
{
w.WriteFullEndElement();
}

public override void WriteProcessingInstruction(string name, string text)
{
w.WriteProcessingInstruction(name, text);
}

public override void WriteRaw(string data)
{
w.WriteRaw(data);
}

public override void WriteRaw(char[] buffer, int index, int count)
{
w.WriteRaw(buffer, index, count);
}

public override void WriteStartAttribute(string prefix, string localName, string ns)
{
w.WriteStartAttribute(prefix, localName, ns);
}

public override void WriteStartDocument(bool standalone)
{
w.WriteStartDocument(standalone);
}

public override void WriteStartDocument()
{
w.WriteStartDocument();
}

public override void WriteStartElement(string prefix, string localName, string ns)
{
w.WriteStartElement(prefix, localName, ns);
}

public override WriteState WriteState
{
get { return w.WriteState; }
}

public override void WriteString(string text)
{
if (WriteState == WriteState.Element)
{
w.WriteCData(text);
}
else
{
w.WriteString(text);
}
}

public override void WriteSurrogateCharEntity(char lowChar, char highChar)
{
w.WriteSurrogateCharEntity(lowChar, highChar);
}

public override void WriteWhitespace(string ws)
{
w.WriteWhitespace(ws);
}
}

You'd then use it like follows:

var serializer = new XmlSerializer(...));
using (var cdataWriter = new XmlCDataWriter(XmlWriter.Create("somepath.xml")))
{
serializer.Serialize(cdataWriter, myDocumentObject);
}

Again, this only makes sense as an option if you want to write everything as CDATA.

Wrap properties with CData Section - XML Serialization C#

With some effort and customization, it possible to get close to what you want, however XmlSerializer will always place the CData nodes at the end of the container element. Your example XML shows the CData nodes between specific nodes of the container element. As long as you don't need this precise control, you can use How do you serialize a string as CDATA using XmlSerializer? to do nested serializations, like so:

public class Order
{
[JsonProperty]
public int OrderId { get; set; }
[JsonProperty]
public string Name { get; set; }
[JsonProperty]
public int Type { get; set; }
[JsonProperty]
public decimal Amount { get; set; }
[JsonProperty]
public DateTime Date { get; set; }

[DataMember]
[JsonProperty]
[XmlIgnore] // Do not serialize directly
[XmlWrapCData] // Instead include in CDATA nodes
public List<Option> ListB { get; set; }

[DataMember]
public List<string> ListC { get; set; }

[XmlIgnore] // Do not serialize directly
[XmlWrapCData] // Instead include in CDATA nodes
public Product Product { get; set; }

[XmlText] // NECESSARY TO EMIT CDATA NODES
[IgnoreDataMember]
[JsonIgnore]
public XmlNode[] CDataContent
{
get
{
return XmlWrapCDataHelper.GetCDataContent(this);
}
set
{
XmlWrapCDataHelper.SetCDataContent(this, value);
}
}
}

public class Product
{
public string ProductId { get; set; }
public string Name { get; set; }
public string Type { get; set; }
}

public class Option
{
public string OptionValue { get; set; }
public string OptionName { get; set; }
}

Using the following extension methods and custom attribute:

[System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple = false)]
public class XmlWrapCDataAttribute : Attribute
{
public XmlWrapCDataAttribute() { this.Namespace = string.Empty; }
public XmlWrapCDataAttribute(string name) : this() { this.Name = name; }

public string Name { get; set; }

public string Namespace { get; set; }
}

public static class XmlWrapCDataHelper
{
static Tuple<PropertyInfo, XmlWrapCDataAttribute> [] XmlWrapCDataProperties(Type type)
{
return type.GetProperties()
.Where(p => p.GetGetMethod() != null && p.GetSetMethod() != null)
.Select(p => Tuple.Create(p, p.GetCustomAttribute<XmlWrapCDataAttribute>()))
.Where(p => p.Item2 != null)
.ToArray();
}

public static XmlNode[] GetCDataContent(object obj)
{
var index = new object[0];
var properties = XmlWrapCDataProperties(obj.GetType());
return properties.Select(p => (XmlNode)p.Item1.GetValue(obj, index).GetCData(p.Item2.Name ?? p.Item1.Name, p.Item2.Namespace)).ToArray();
}

public static void SetCDataContent(object obj, XmlNode [] nodes)
{
if (nodes == null || nodes.Length < 1)
return;
var index = new object[0];
var properties = XmlWrapCDataProperties(obj.GetType()).ToDictionary(p => XName.Get(p.Item2.Name ?? p.Item1.Name, p.Item2.Namespace), p => p);
var xml = "<Root>" + String.Concat(nodes.Select(c => c.Value)) + "</Root>";
foreach (var element in XElement.Parse(xml).Elements())
{
Tuple<PropertyInfo, XmlWrapCDataAttribute> pair;
if (properties.TryGetValue(element.Name, out pair))
{
var value = element.Deserialize(pair.Item1.PropertyType, element.Name.LocalName, element.Name.Namespace.NamespaceName);
pair.Item1.SetValue(obj, value, index);
}
}
}
}

public static class XmlSerializationHelper
{
public static XmlCDataSection GetCData(this object obj, string rootName, string rootNamespace)
{
return obj == null ? null : new System.Xml.XmlDocument().CreateCDataSection(obj.GetXml(XmlSerializerFactory.Create(obj.GetType(), rootName, rootNamespace)));
}

public static XCData GetCData(this object obj, XmlSerializer serializer = null)
{
return obj == null ? null : new XCData(obj.GetXml(serializer));
}

public static string GetXml(this object obj, XmlSerializer serializer = null)
{
using (var textWriter = new StringWriter())
{
var ns = new XmlSerializerNamespaces();
ns.Add("", ""); // Disable the xmlns:xsi and xmlns:xsd lines.
var settings = new XmlWriterSettings() { Indent = true, IndentChars = " ", OmitXmlDeclaration = true }; // For cosmetic purposes.
using (var xmlWriter = XmlWriter.Create(textWriter, settings))
(serializer ?? new XmlSerializer(obj.GetType())).Serialize(xmlWriter, obj, ns);
return textWriter.ToString();
}
}

public static object Deserialize(this XContainer element, Type type, string rootName = null, string rootNamespace = null)
{
return element.Deserialize(type, XmlSerializerFactory.Create(type, rootName, rootNamespace));
}

public static object Deserialize(this XContainer element, Type type, XmlSerializer serializer = null)
{
using (var reader = element.CreateReader())
{
return (serializer ?? new XmlSerializer(type)).Deserialize(reader);
}
}

public static T DeserializeXML<T>(this string xmlString, XmlSerializer serializer = null)
{
using (StringReader reader = new StringReader(xmlString))
{
return (T)(serializer ?? new XmlSerializer(typeof(T))).Deserialize(reader);
}
}
}

public static class XmlSerializerFactory
{
readonly static Dictionary<Tuple<Type, string, string>, XmlSerializer> cache;
readonly static object padlock;

static XmlSerializerFactory()
{
padlock = new object();
cache = new Dictionary<Tuple<Type, string, string>, XmlSerializer>();
}

public static XmlSerializer Create(Type serializedType, string rootName, string rootNamespace)
{
if (serializedType == null)
throw new ArgumentNullException();
if (rootName == null && rootNamespace == null)
return new XmlSerializer(serializedType);
lock (padlock)
{
XmlSerializer serializer;
var key = Tuple.Create(serializedType, rootName, rootNamespace);
if (!cache.TryGetValue(key, out serializer))
cache[key] = serializer = new XmlSerializer(serializedType, new XmlRootAttribute { ElementName = rootName, Namespace = rootNamespace });
return serializer;
}
}
}

This will parse your provided XML successfully, and in return generate XML that looks like:

<Order>
<OrderId>2</OrderId>
<Name>Some Name</Name>
<Type>1</Type>
<Amount>100</Amount>
<Date>2015-12-07T05:10:49.6031106-05:00</Date>
<ListC>
<string>ListItem1</string>
<string>ListItem2</string>
</ListC><![CDATA[<ListB>
<Option>
<OptionValue>OptionValue1</OptionValue>
<OptionName>Option1</OptionName>
</Option>
<Option>
<OptionValue>OptionValue2</OptionValue>
<OptionName>Option2</OptionName>
</Option>
</ListB>]]><![CDATA[<Product>
<ProductId>1</ProductId>
<Name>ProductName</Name>
<Type>Product Type</Type>
</Product>]]></Order>

XmlSerialize Class to CDATA

One of the ways to get the output you are expecting is to separate the creation of the module data and generating the CDATA part. For example:

To create the module data -

  1. Create a class to hold module details as below -

    public class Module
    {
    public string title { get; set; }
    public string code { get; set; }
    public string level { get; set; }
    public string summary { get; set; }
    }
  2. Create a method to fetch these datails -

     public static string CreateXMLString()
    {
    List<Module> modules = new List<Module>();
    modules = new List<Module>() { new Module() { code = "test",
    summary="Test3", title="Test", level = "tests1" },
    new Module() { code = "test3",
    summary="Test3", title="Test3", level = "tests3" } };

    // Create XML to return the string in the format of
    // <module code="test">
    // < level > tests1 </ level >
    // < summary > Test3 </ summary >
    // < title > Test </ title >
    //</ module >< module code = "test3" >
    // < level > tests3 </ level >
    // < summary > Test3 </ summary >
    // < title > Test3 </ title >
    // </ module >

    var modulesXml =
    from mod in modules
    select new XElement("module",
    new XAttribute("code", mod.code),
    new XElement("level", mod.level),
    new XElement("summary", mod.summary),
    new XElement("title", mod.title)
    );

    return String.Concat(modulesXml);
    }

To get the CDATA you can use the below steps -


  1. Create a class Modulesand refer the documentation for usages of CreateCDataSection and for similar threads here for the details

    [XmlType("")]
    public class Modules
    {
    public Modules() { }

    [XmlIgnore]
    public string Message { get; set; }
    [XmlElement("modules")]
    public System.Xml.XmlCDataSection MyStringCDATA
    {
    get
    {
    return new System.Xml.XmlDocument().CreateCDataSection(Message);
    }
    set
    {
    Message = value.Value;
    }
    }
    }

To test the output assign the string generated in step 2 during serialization you can refer the sample code below

static void Main(string[] args)
{
Modules mc = new Modules();
mc.Message = CreateXMLString();//Assign your data created in step 2
XmlSerializer serializer = new XmlSerializer(typeof(Modules));
XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
ns.Add("", "");

StringWriter writer = new StringWriter();

//Remove unnecessary namespaces
serializer.Serialize(writer, mc,ns);
var test = XDocument.Parse(writer.ToString());

var data = test.Root.Elements();

Console.WriteLine(data.FirstOrDefault().Value);

}

Output -

<modules>
<![CDATA[<module>
<code>test</code>
<level>tests1</level>
<summary>Test3</summary>
<title>Test</title>
</module><module>
<code>test3</code>
<level>tests3</level>
<summary>Test3</summary>
<title>Test3</title>
</module>]]>
</modules>

Problems serializing a class to XML and including a CDATA section

In response to the 'spaces' you are seeing after your edit, it is because of the encoding you are using (Unicode, 2 bytes per character).

Try:

settings.Encoding = new Utf8Encoding(false);

EDIT:

Also, note that format of the MemoryStream is not necessarily a valid UTF-8 encoded string! You can use a StringBuilder instead of MemoryStream to create your inner writer.

    public void WriteXml(XmlWriter writer)   
{
XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
ns.Add("", "");

XmlWriterSettings settings = new XmlWriterSettings();

settings.OmitXmlDeclaration = true;
settings.Indent = true;

StringBuilder sb = new StringBuilder();
using (XmlWriter innerWriter = XmlWriter.Create(sb, settings))
{
shipmentInfoSerializer.Serialize(innerWriter, this.Shipment,ns);
innerWriter.Flush();
writer.WriteCData(sb.ToString());
}
}

Javascript serializing CDATA with XMLSerializer

I came up with a hacky solution, which is not really satisfying, but it works.

Before serialization the cdata's data is surrounded by a start and end marker (I am using __CDATA and CDATA__ and after serialization a regular expression is used to replace the markers (and optional CDATA opening and end tags) by CDATA opening and end tags.

Optionally browser sniffing could be used to detect if this hack is necessary at all.

Here is the modified example:

var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");var xml = new DOMParser().parseFromString('<xml></xml>',  "application/xml");var cdata = xml.createCDATASection('Some data & some more');// Add markerscdata.data = "__CDATA" + cdata.data + "CDATA__";svg.appendChild(cdata);var svgStr = new XMLSerializer().serializeToString(svg);// Replace markers (and already existing CDATA start and end tags)svgStr = svgStr.replace(/(?:<\!\[CDATA\[)?__CDATA(.*?)CDATA__(?:\]\]>)?/g, '<![CDATA[$1]]>');
log("SVG: " + svgStr);


// Utility function for outputfunction log(s) { var output = document.getElementById('output'); if (output) { output.innerHTML += new Option(s).innerHTML + '<br>'; } else { console.log(s); }}
<div id="output"></div>


Related Topics



Leave a reply



Submit