KevinKode
KevinKode

Reputation: 33

Xml.Serialization object T with attribute value

i am using Xml Attributes on my Models to handle serializing my Models.

the base class is:

public class RequestRoot<T>
{
    [XmlElement("Action")]
    public T ActionNode { get; set; }
}

ActionNode is of type T, and can be anything from a string to a collection of complex objects.

example:

<?xml version="1.0" encoding="UTF-8"?>
<RequestRoot>
<Action Type="TypeValue A">
    <SomeData>Some data</SomeData>
</Action>
</RequestRoot>

<?xml version="1.0" encoding="UTF-8"?>
<RequestRoot>
<Action Type="TypeValue B">
    <MyObjects>
        <MyObject>
            <ObjectValue1>Object Value 1-1</ObjectValue1>
            <ObjectValue2>Object Value 2-1</ObjectValue2>
        </MyObject>
        <MyObject>
            <ObjectValue1>Object Value 1-2</ObjectValue1>
            <ObjectValue2>Object Value 2-2</ObjectValue2>
        </MyObject>
    </MyObjects>
</Action>
</RequestRoot>

my question is this: is it possible to use Xml Attributes on my Models to write Type="TypeValue A" or Type="TypeValue B" depending upon what T is?

if not, what alternatives do i have?

Upvotes: 1

Views: 206

Answers (1)

dbc
dbc

Reputation: 116526

There is no way to do this with XmlSerializer out of the box. That's because your RequestRoot class is generic, and XmlSerializer determines the type of object to create based on the XML element name and, possibly, the "xsi:type" attribute. Your type information, however, is embedded in a sub-element Action of the root element which is not accessible at the time the root must needs be allocated.

What you will have to do is to read and write the RequestRoot wrapper manually, then use XmlSerializer for the contents. For instance:

public abstract class RequestRootBase
{
    [XmlIgnore]
    public abstract Type RequestType { get; }

    [XmlIgnore]
    public abstract Object RequestObject { get; }
}

public class RequestRoot<T> : RequestRootBase
{
    public RequestRoot() { }

    public RequestRoot(T ActionNode) { this.ActionNode = ActionNode; }

    [XmlElement("Action")]
    public T ActionNode { get; set; }

    public override Type RequestType
    {
        get { return typeof(T); }
    }

    public override object RequestObject
    {
        get { return ActionNode; }
    }
}

public static class RequestRootHelper
{
    public static RequestRootBase CreateBase(object action)
    {
        if (action == null)
            throw new ArgumentNullException();
        var type = action.GetType();
        return (RequestRootBase)Activator.CreateInstance(typeof(RequestRoot<>).MakeGenericType(type), new [] { action });
    }

    public static RequestRoot<T> Create<T>(T action)
    {
        return new RequestRoot<T> { ActionNode = action };
    }
}

public abstract class RequestRootXmlSerializerBase
{
    const string RequestRootElementName = "RequestRoot";
    const string ActionElementName = "Action";
    const string TypeAttributeName = "Type";

    protected abstract Type BindToType(string name);

    protected abstract string BindToName(Type type);

    static string DefaultRootXmlElementNamespace(Type type)
    {
        var xmlType = type.GetCustomAttribute<XmlRootAttribute>();
        if (xmlType != null && !string.IsNullOrEmpty(xmlType.Namespace))
            return xmlType.Namespace;
        return null;
    }

    public void Serialize(RequestRootBase root, XmlWriter writer)
    {
        writer.WriteStartDocument();
        writer.WriteStartElement(RequestRootElementName);
        writer.WriteStartElement(ActionElementName);
        var typeName = BindToName(root.RequestType);
        writer.WriteAttributeString(TypeAttributeName, typeName);

        var serializer = new XmlSerializer(root.RequestType);

        var rootNameSpace = DefaultRootXmlElementNamespace(root.RequestType);
        var ns = new XmlSerializerNamespaces();
        if (string.IsNullOrEmpty(rootNameSpace))
            ns.Add("", "");
        else
            ns.Add("", rootNameSpace);

        serializer.Serialize(writer, root.RequestObject, ns);

        writer.WriteEndElement();
        writer.WriteEndElement();
        writer.WriteEndDocument();
    }

    public RequestRootBase Deserialize(XmlReader reader)
    {
        if (!reader.ReadToFollowing(RequestRootElementName))
            return null;
        if (!reader.ReadToFollowing(ActionElementName))
            return null;
        var typeName = reader[TypeAttributeName];
        if (typeName == null)
            return null;

        var type = BindToType(typeName);
        if (type == null)
            throw new InvalidDataException();  // THROW AN EXCEPTION in this case

        reader.ReadStartElement();

        var serializer = new XmlSerializer(type);

        var action = serializer.Deserialize(reader);
        if (action == null)
            return null;

        return RequestRootHelper.CreateBase(action);
    }

    public string SerializeToString(RequestRootBase root)
    {
        using (var textWriter = new StringWriter())
        {
            var settings = new XmlWriterSettings() { Indent = true, IndentChars = "    " }; // For cosmetic purposes.
            using (var xmlWriter = XmlWriter.Create(textWriter, settings))
                Serialize(root, xmlWriter);
            return textWriter.ToString();
        }
    }

    public RequestRootBase DeserializeFromString(string xml)
    {
        using (var sr = new StringReader(xml))
        using (var xmlReader = XmlReader.Create(sr))
        {
            return Deserialize(xmlReader);
        }
    }
}

public class RequestRootXmlSerializer : RequestRootXmlSerializerBase
{
    readonly Dictionary<string, Type> nameToType = new Dictionary<string, Type>();
    readonly Dictionary<Type, string> typeToName = new Dictionary<Type, string>();

    const string ListPrefix = "ArrayOf";
    const string ListPostFix = "";

    protected override string BindToName(Type type)
    {
        return typeToName[type];
    }

    protected override Type BindToType(string name)
    {
        return nameToType[name];
    }

    public RequestRootXmlSerializer(IEnumerable<Type> types)
    {
        if (types == null)
            throw new ArgumentNullException();
        foreach (var type in types)
        {
            if (type.IsInterface || type.IsAbstract)
                throw new ArgumentException();
            var name = DefaultXmlElementName(type);
            nameToType.Add(name, type);
            typeToName.Add(type, name);
        }
    }

    static string DefaultXmlElementName(Type type)
    {
        if (type.IsGenericType
            && type.GetGenericTypeDefinition() == typeof(List<>))
        {
            var elementType = type.GetGenericArguments()[0];
            return ListPrefix + DefaultXmlElementName(elementType) + ListPostFix;
        }
        else
        {
            var xmlRoot = type.GetCustomAttribute<XmlRootAttribute>();
            if (xmlRoot != null && !string.IsNullOrEmpty(xmlRoot.ElementName))
                return xmlRoot.ElementName;
            var xmlType = type.GetCustomAttribute<XmlTypeAttribute>();
            if (xmlType != null && !string.IsNullOrEmpty(xmlType.TypeName))
                return xmlType.TypeName;
            return type.Name;
        }
    }
}

You will probably want to replace my type-to-name mapping scheme with your own; it's just a prototype.

Then use it like:

[XmlRoot("A", Namespace="ATestNameSpace")]
public class ClassA
{
    [XmlText]
    public string Value { get; set; }
}

public class MyObject
{
    public string ObjectValue1 { get; set; }
    public string ObjectValue2 { get; set; }
}

public class TestClass
{
    public static void Test()
    {
        var root1 = RequestRootHelper.Create(new ClassA { Value = "Some data" });
        var root2 = RequestRootHelper.Create(new List<MyObject> { new MyObject { ObjectValue1 = "Object Value 1-1", ObjectValue2 = "Object Value 2-1" }, new MyObject { ObjectValue1 = "Object Value 1-2", ObjectValue2 = "Object Value 2-2" } });

        var serializer = new RequestRootXmlSerializer(new[] { typeof(ClassA), typeof(List<ClassA>), typeof(MyObject), typeof(List<MyObject>) });

        TestRootSerialization(root1, serializer);

        TestRootSerialization(root2, serializer);
    }

    private static void TestRootSerialization<T>(RequestRoot<T> root, RequestRootXmlSerializer serializer)
    {
        var xml1 = serializer.SerializeToString(root);
        Debug.WriteLine(xml1);
        var root11 = serializer.DeserializeFromString(xml1);
        Debug.Assert(root.GetType() == root11.GetType()); // NO ASSERT
        var xml11 = serializer.SerializeToString(root11);
        Debug.WriteLine(xml11);
        Debug.Assert(xml1 == xml11); // NO ASSERT
    }
}

This produces the following XML output for ClassA:

<RequestRoot>
    <Action Type="A">
        <A xmlns="ATestNameSpace">Some data</A>
    </Action>
</RequestRoot>

And for List<MyObject>:

<RequestRoot>
    <Action Type="ArrayOfMyObject">
        <ArrayOfMyObject>
            <MyObject>
                <ObjectValue1>Object Value 1-1</ObjectValue1>
                <ObjectValue2>Object Value 2-1</ObjectValue2>
            </MyObject>
            <MyObject>
                <ObjectValue1>Object Value 1-2</ObjectValue1>
                <ObjectValue2>Object Value 2-2</ObjectValue2>
            </MyObject>
        </ArrayOfMyObject>
    </Action>
</RequestRoot>

Upvotes: 1

Related Questions