Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
930 views
in Technique[技术] by (71.8m points)

c# - Deserialize XML with XmlSerializer where XmlElement names differ but have same content

I would like to deserialize an XML File to a class with several subclasses. The XML looks like this:

<?xml version="1.0" encoding="utf-8"?>
<Objects>
    <Group index="1">
        <de>
            <GroupName>ANTRIEB</GroupName>
        </de>
        <en>
            <GroupName>missing translation!</GroupName>
        </en>
        <Level>2</Level>
    </Group>
    <Group index="2">
        <de>
            <GroupName>BREMSEN</GroupName>
        </de>
        <Level>3</Level>
    </Group>
</Objects>

Deserializing the XML to classes would be no problem, if there wouldn't be those language tags. Sure, I could create a property for every language tag possible. But the list of languages possible should be dynamic (e.g. read from an config file).

This is the reason why i would like to deserialize those language tags and their content into a Dictionary which uses the language as key and a model for the content.

My models look like this:

[XmlRoot("Objects")]
public class DeactivationsXml
{
    [XmlElement("Group")]
    public DeactivationsGroup[] Groups { get; set; }
}

[Serializable()]
public class DeactivationsGroup
{
    [XmlIgnore]
    public Dictionary<string, GroupName> GroupNames { get; set; } = new Dictionary<string, GroupName>();

    public int Level { get; set; }

    [XmlAttribute]
    public byte index { get; set; }
}

public class GroupName
{
    [XmlElement("GroupName")]
    public string Name { get; set; }
}

I searched for a long time to address this problem, but couldn't find a solution. I'm pretty sure, that it's not possible to solve this Problem just with attributes.

Does some hybrid aproach exist in order to combine the Deserialization of an XML File in combination with manual deserialization of all XmlElements which could not be automatically deserialized?

A good and extensible solution for my problem would be great, because the XML structure is complex (same Problem several times with different content etc.). I can't change the structure of the XML, so please don't point this out.

Approaches

IXmlSerializable

I tried to implement the IXmlSerializable Interface on the DeactivationsGroup class in order to search with a list of given languages for XmlElements with those names and deserialize the content of those XmlElements.

But this approach didn't work out, because you have to map all properties manually.

IExtensibleDataObject

The Interface is only supported by a DataContractSerializer. In the worst case i could use this interface to deserialize after Deserializing, if no other solution is found..

OnDeserialization

This Attribute is not supported by XmlSerializer, but would provide the functionality i possibly need.

XmlAnyElement

I guess this is the best option at this point. Does some callback exist after deserialization finished in order to automate this?

Executable Code

Here's the whole code so far.

public void Parse()
{
    string xml = "<?xml version="1.0" encoding="utf-8"?>" + 
"    <Objects>" + 
"       <Group index="1">" + 
"           <de>" + 
"               <GroupName>ANTRIEB</GroupName>" + 
"           </de>" + 
"           <en>" + 
"               <GroupName>missing translation!</GroupName>" + 
"           </en>" + 
"           <Level>2</Level>" + 
"       </Group>" + 
"       <Group index="2">" + 
"           <de>" + 
"               <GroupName>BREMSEN</GroupName>" + 
"           </de>" + 
"           <Level>3</Level>" + 
"       </Group>" + 
"    </Objects>";

    XmlSerializer serializer = new XmlSerializer(typeof(DeactivationsXml));

    using (TextReader fileStream = new StringReader(xml))
    {
        var result = (DeactivationsXml)serializer.Deserialize(fileStream);
    }
}

[XmlRoot("Objects")]
public class DeactivationsXml
{
    [XmlElement("Group")]
    public DeactivationsGroup[] Groups { get; set; }
}

[Serializable()]
public class DeactivationsGroup
{
    [XmlIgnore]
    public Dictionary<string, GroupName> GroupNames { get; set; } = new Dictionary<string, GroupName>();

    public int Level { get; set; }

    [XmlAttribute]
    public byte index { get; set; }
}

public class GroupName
{
    [XmlElement("GroupName")]
    public string Name { get; set; }
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

You can adopt the approach from this answer and add a surrogate XmlElement [] property, marked with [XmlAnyElement], that performs a nested (de)serialization on the key/value pairs of the Dictionary<string, GroupName> property, binding the dictionary keys to the element names.

Note that, while the documentation for XmlAnyElementAttribute states

Specifies that the member (a field that returns an array of XmlElement or XmlNode objects) contains objects that represent any XML element that has no corresponding member in the object being serialized or deserialized.

In fact the attribute can be applied to a property as well. Thus a (de)serialization callback is not required since the nested serialization can be performed inside the getter and setter for the surrogate property itself. It can also be applied to members returning an array of XElement objects instead of XmlElement if you prefer the new LINQ-to-XML API.

In this approach, your DeactivationsGroup would look like:

[Serializable()]
public class DeactivationsGroup
{
    public DeactivationsGroup() { this.GroupNames = new Dictionary<string, GroupName>(); }

    [XmlIgnore]
    public Dictionary<string, GroupName> GroupNames { get; set; }

    public int Level { get; set; }

    [XmlAttribute]
    public byte index { get; set; }

    [XmlAnyElement]
    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public XElement[] XmlGroupNames
    {
        get
        {
            return GroupNames.SerializeToXElements(null);
        }
        set
        {
            if (value == null || value.Length < 1)
                return;
            foreach (var pair in value.DeserializeFromXElements<GroupName>())
            {
                GroupNames.Add(pair.Key, pair.Value);
            }
        }
    }
}

Making use of the following extension methods and classes:

public static class XmlKeyValueListHelper
{
    const string RootLocalName = "Root";

    public static XElement [] SerializeToXElements<T>(this IEnumerable<KeyValuePair<string, T>> dictionary, XNamespace ns)
    {
        if (dictionary == null)
            return null;
        ns = ns ?? "";
        var serializer = XmlSerializerFactory.Create(typeof(T), RootLocalName, ns.NamespaceName);
        var array = dictionary
            .Select(p => new { p.Key, Value = p.Value.SerializeToXElement(serializer, true) })
            // Fix name and remove redundant xmlns= attributes.  XmlWriter will add them back if needed.
            .Select(p => new XElement(ns + p.Key, p.Value.Attributes().Where(a => !a.IsNamespaceDeclaration), p.Value.Elements()))
            .ToArray();
        return array;
    }

    public static IEnumerable<KeyValuePair<string, T>> DeserializeFromXElements<T>(this IEnumerable<XElement> elements)
    {
        if (elements == null)
            yield break;
        XmlSerializer serializer = null;
        XNamespace ns = null;
        foreach (var element in elements)
        {
            if (serializer == null || element.Name.Namespace != ns)
            {
                ns = element.Name.Namespace;
                serializer = XmlSerializerFactory.Create(typeof(T), RootLocalName, ns.NamespaceName);
            }
            var elementToDeserialize = new XElement(ns + RootLocalName, element.Attributes(), element.Elements());
            yield return new KeyValuePair<string, T>(element.Name.LocalName, elementToDeserialize.Deserialize<T>(serializer));
        }
    }

    public static XmlSerializerNamespaces NoStandardXmlNamespaces()
    {
        var ns = new XmlSerializerNamespaces();
        ns.Add("", ""); // Disable the xmlns:xsi and xmlns:xsd lines.
        return ns;
    }

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

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

    public static XElement SerializeToXElement<T>(this T obj, XmlSerializer serializer, bool omitStandardNamespaces)
    {
        return obj.SerializeToXElement(serializer, (omitStandardNamespaces ? NoStandardXmlNamespaces() : null));
    }

    public static XElement SerializeToXElement<T>(this T obj, XmlSerializer serializer, XmlSerializerNamespaces ns)
    {
        var doc = new XDocument();
        using (var writer = doc.CreateWriter())
            (serializer ?? new XmlSerializer(obj.GetType())).Serialize(writer, obj, ns);
        var element = doc.Root;
        if (element != null)
            element.Remove();
        return element;
    }

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

public static class XmlSerializerFactory
{
    // To avoid a memory leak the serializer must be cached.
    // https://stackoverflow.com/questions/23897145/memory-leak-using-streamreader-and-xmlserializer
    // This factory taken from 
    // https://stackoverflow.com/questions/34128757/wrap-properties-with-cdata-section-xml-serialization-c-sharp/34138648#34138648

    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;
        }
    }
}

Sample fiddle. And another demonstrating a case with XML namespaces and attributes.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...