1

I need to serialize my object in such a way that the properties I want, would get wrapped around CData sections. I was hoping I could do something like this :

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]
        **[WrapCData]**
        public List<Option> ListB { get; set; }
        [DataMember]
        public List<string> ListC { get; set; }
        **[WrapCData]**
        public Product Product { get; set; }
    }

Is there any attribute or an implementation which could wrap my specific properties around a CData section? Existing StackOverflow answers suggest fiddling with the Entity(Class) itself. This would get really messy.

In the following thread : How do you serialize a string as CDATA using XmlSerializer?

Philip's answer suggests to make another property and its corresponding CData property. However the property is a string. CreateCDataSection() also takes a string. I need to wrap my custom objects/lists around CDataSections. How can I do that? Any help would be appreciated. Thanks.

Sample XML for the above Order Class:

<Order xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <OrderId>2</OrderId>
  <Name>Some Name</Name>
  <Type>1</Type>
  <Amount>100</Amount>
  <Date>2015-12-07T15:10:49.6031106+05:00</Date>
  <![CDATA[
<ListB>
    <Option>
      <OptionValue>OptionValue1</OptionValue>
      <OptionName>Option1</OptionName>
    </Option>
    <Option>
      <OptionValue>OptionValue2</OptionValue>
      <OptionName>Option2</OptionName>
    </Option>
  </ListB>
]]>
  <ListC>
    <string>ListItem1</string>
    <string>ListItem2</string>
  </ListC>
  <![CDATA[
  <Product>
    <ProductId>1</ProductId>
    <Name>ProductName</Name>
    <Type>Product Type</Type>
  </Product>
]]>
</Order>
Community
  • 1
  • 1
zeppelin
  • 451
  • 1
  • 4
  • 24
  • What do you mean by *wrap my specific properties around a CData section*? Can you give a simple example of a class and the XML you want to generate for this class? – dbc Dec 07 '15 at 08:17
  • @dbc I have updated the question & added a sample xml output after serialization from Order class. – zeppelin Dec 07 '15 at 10:14
  • @dbc `Product` & `Option` are my custom POCO classes. I want them to be wrapped with CData sections during serialization using `XmlSerializer`(in my case) or any other library that can do that. – zeppelin Dec 07 '15 at 10:23
  • How important is it for you to have the `CDATA` sections *in the middle* of the containing element? Could they all come at the end? – dbc Dec 07 '15 at 15:31

1 Answers1

2

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>
dbc
  • 104,963
  • 20
  • 228
  • 340
  • Thanks alot for the detailed response. I may not be able to test this immeditely as I am engaged in something else right now. But from the looks of it, it will work. Thank you again! – zeppelin Dec 09 '15 at 10:57
  • Is there any other best way to achieve this? Or do we have class / library to do this in C#. – Albert Einstein Dec 27 '19 at 10:56