3

I want to create a xml for a generic class. One of the properties has the generic type. For this property I don't want to use the property name as its XML element name, but the name of the generic type.

The class looks like this:

[XmlRoot("Entity")]
public class StoreItem<TEntity>
    where TEntity : class, new()
{
    /// <summary>
    /// Gets and sets the status of the entity when storing.
    /// </summary>
    [XmlAttribute]
    public System.Data.Services.Client.EntityStates Status { get; set; }

    /// <summary>
    /// Gets and sets the entity to be stored.
    /// </summary>
    public TEntity Entity { get; set; }
}

When serializing a store item of kind StoreItem<SewageArea> the XML should contain something like:

<Entity Status="Deleted">
    <SewageArea ...>
       ...
    </SewageArea>
<Entity>

The requirement is, that the SewageArea in the above example should be serialized in the "normal" way. Another important thing is that if its possible the code should be prepared to automatically serializes new added properties at the StoreItemclass.

scher
  • 1,813
  • 2
  • 18
  • 39
  • This is not so easy. You'd like to do something along the lines of [Rename class when serializing to XML](https://stackoverflow.com/questions/36745144/rename-class-when-serializing-to-xml) but you cannot because attribute arguments cannot contain generic type parameters, i.e. `[XmlElement(typeof(TEntity))]` will not compile. – dbc Jul 13 '16 at 07:19
  • I know, that it is not straight forward. I thought about implementing the IXmlSerialisable interface but hang when doing the "normal" serialization of the entity object and the automatic serialization of the other properties. – scher Jul 13 '16 at 07:25

1 Answers1

9

You'd like to do something along the lines of Rename class when serializing to XML but you cannot because attribute arguments cannot contain generic type parameters, i.e. [XmlElement(typeof(TEntity))]. And the obvious alternative of implementing IXmlSerializable is inconvenient because you lose automatic serialization of properties subsequently added to StoreItem<TEntity>.

Instead, what you can do is to make use of an [XmlAnyElement] surrogate property to do a nested serialization of your TEntity, as follows:

[XmlRoot("Entity")]
public class StoreItem<TEntity>
    where TEntity : class, new()
{
    /// <summary>
    /// Gets and sets the status of the entity when storing.
    /// </summary>
    [XmlAttribute]
    public System.Data.Services.Client.EntityStates Status { get; set; }

    /// <summary>
    /// Gets and sets the entity to be stored.
    /// </summary>
    [XmlIgnore]
    public TEntity Entity { get; set; }

    [XmlAnyElement]
    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public XElement XmlEntity
    {
        get
        {
            return (Entity == null ? null : XObjectExtensions.SerializeToXElement(Entity, null, true));
        }
        set
        {
            Entity = (value == null ? null : XObjectExtensions.Deserialize<TEntity>(value));
        }
    }
}

Using the extension methods:

public static class XObjectExtensions
{
    public static T Deserialize<T>(this XContainer element)
    {
        return element.Deserialize<T>(null);
    }

    public static T Deserialize<T>(this XContainer element, XmlSerializer serializer)
    {
        using (var reader = element.CreateReader())
        {
            serializer = serializer ?? new XmlSerializer(typeof(T));
            object result = serializer.Deserialize(reader);
            if (result is T)
                return (T)result;
        }
        return default(T);
    }

    public static XElement SerializeToXElement<T>(this T obj)
    {
        return obj.SerializeToXElement(null, true);
    }

    public static XElement SerializeToXElement<T>(this T obj, XmlSerializer serializer, bool omitStandardNamespaces)
    {
        var doc = new XDocument();
        using (var writer = doc.CreateWriter())
        {
            XmlSerializerNamespaces ns = null;
            if (omitStandardNamespaces)
                (ns = new XmlSerializerNamespaces()).Add("", ""); // Disable the xmlns:xsi and xmlns:xsd lines.
            serializer = serializer ?? new XmlSerializer(obj.GetType());
            serializer.Serialize(writer, obj, ns);
        }
        var element = doc.Root;
        if (element != null)
            element.Remove();
        return element;
    }
}

Note that the [XmlAnyElement] property will be called for all unknown elements, so if your XML for some reason has unexpected elements, you may get an exception thrown from XObjectExtensions.Deserialize<TEntity>(value)) because the root element name is wrong. You may want to catch and ignore exceptions from this method if that is a possibility.

Then, for the sample TEntity class

public class SewageArea
{
    public double Area { get; set; }
}

The XML output is:

<Entity Status="State1">
  <SewageArea>
    <Area>10101</Area>
  </SewageArea>
</Entity>

Sample fiddle.

dbc
  • 104,963
  • 20
  • 228
  • 340