How to define multiple names for XmlElement field?

12,115

Solution 1

Take 2 - let's implement this ourselves using the unknown element handling event (see the comments below for some limitations though):

public class XmlSynonymDeserializer : XmlSerializer
{
    public class SynonymsAttribute : Attribute
    {
        public readonly ISet<string> Names;

        public SynonymsAttribute(params string[] names)
        {
            this.Names = new HashSet<string>(names);
        }

        public static MemberInfo GetMember(object obj, string name)
        {
            Type type = obj.GetType();

            var result = type.GetProperty(name);
            if (result != null)
                return result;

            foreach (MemberInfo member in type.GetProperties().Cast<MemberInfo>().Union(type.GetFields()))
                foreach (var attr in member.GetCustomAttributes(typeof(SynonymsAttribute), true))
                    if (attr is SynonymsAttribute && ((SynonymsAttribute)attr).Names.Contains(name))
                        return member;

            return null;
        }
    }

    public XmlSynonymDeserializer(Type type)
        : base(type)
    {
        this.UnknownElement += this.SynonymHandler;
    }

    public XmlSynonymDeserializer(Type type, XmlRootAttribute root)
        : base(type, root)
    {
        this.UnknownElement += this.SynonymHandler;
    }

    protected void SynonymHandler(object sender, XmlElementEventArgs e)
    {
        var member = SynonymsAttribute.GetMember(e.ObjectBeingDeserialized, e.Element.Name);
        Type memberType;

        if (member != null && member is FieldInfo)
            memberType = ((FieldInfo)member).FieldType;
        else if (member != null && member is PropertyInfo)
            memberType = ((PropertyInfo)member).PropertyType;
        else
            return;

        if (member != null)
        {
            object value;
            XmlSynonymDeserializer serializer = new XmlSynonymDeserializer(memberType, new XmlRootAttribute(e.Element.Name));
            using (System.IO.StringReader reader = new System.IO.StringReader(e.Element.OuterXml))
                value = serializer.Deserialize(reader);

            if (member is FieldInfo)
                ((FieldInfo)member).SetValue(e.ObjectBeingDeserialized, value);
            else if (member is PropertyInfo)
                ((PropertyInfo)member).SetValue(e.ObjectBeingDeserialized, value);
        }
    }
}

And now the actual code of the class would be:

[XmlRoot]
public class SomeAccount
{
    [XmlElement("parentId")]
    [XmlSynonymDeserializer.Synonyms("LeParentId", "AnotherGreatName")]
    public long ParentId { get; set; }
    //rest of fields...
}

To deserialize, simply use XmlSynonymDeserializer instead of the regular XmlSerializer. This should work for most of the basic needs.

Known limitations:

  • This implementation supports only elements with multiple names; extending it for attributes should be trivial
  • Support for handling of properties/fields in cases where the entities inherit from one another is not tested
  • This implementation does not check for programming bugs (having the attribute on read-only/constant field/properties, multiple members with the same synonyms and so on)

Solution 2

I know this is an old post, but maybe this will help anyone else having the same problem. What you could use for this problem is XmlChoiceIdentifier.

[XmlRoot]
public class SomeAccount
{
    [XmlIgnore]
    public ItemChoiceType EnumType;

    [XmlChoiceIdentifier("EnumType")]
    [XmlElement("LeParentId")]
    [XmlElement("parentId")]
    public long ParentId { get; set; }

    //rest of fields...
} 
[XmlType(IncludeInSchema = false)]
public enum ItemChoiceType
{
    LeParentId,
    parentId
}

Now if you have a new xml version and a new XmlElement name you just add that name to the ItemChoiceType enum and a new XmlElement to the property.

Solution 3

If you need only one more name, here is a quick (and rather ugly) solution that we deployed in several cases in my work when we had only to read XMLs (this will be problematic for serializing back to an XML), because it's the simplest and easiest to understand:

[XmlRoot]
public class SomeAccount
{
    [XmlElement("parentId")]
    public long ParentId { get; set; }
    [XmlElement("LeParentId")]
    public long LeParentId { get { return this.ParentId; } set { this.ParentId = value; } }
    //rest of fields...
}
Share:
12,115
Luiggi Mendoza
Author by

Luiggi Mendoza

🤯 Wild software engineer unleashed in a crazy world 📺 One Piece, FMAB 🎮 Souls games 🧑🏽‍🍳 Peruvian &amp; keto food (kinda) 🌱 Gardener wannabe You can contact me anytime to [email protected]. My LinkedIn Profile.

Updated on June 24, 2022

Comments

  • Luiggi Mendoza
    Luiggi Mendoza about 2 years

    I have a XML document provided by client applications to my C# application. This is how a client sends the XML file:

    <?xml version="1.0" encoding="utf-8"?>
    <SomeAccount>
        <parentId>2380983</parentId>
        <!-- more elements -->
    </SomeAccount>
    

    And a C# class that supports the XML deserialization:

    [XmlRoot]
    public class SomeAccount
    {
        [XmlElement("parentId")]
        public long ParentId { get; set; }
        //rest of fields...
    }
    

    But there are some clients whose system send the XML in this way (note the upper case in LeParentId):

    <?xml version="1.0" encoding="utf-8"?>
    <SomeAccount>
        <LeParentId>2380983</LeParentId>
        <!-- similar for the other elements -->
    </SomeAccount>
    

    How can I make this field (and others) to support both XML names parentId and LeParentId?

    This is the method I'm currently using for XML deserialization:

    public sealed class XmlSerializationUtil
    {
        public static T Deserialize<T>(string xml)
        {
            if (xml == null)
                return default(T);
            XmlSerializer serializer = new XmlSerializer(typeof(T));
            StringReader stringReader = new StringReader(xml);
            return (T)serializer.Deserialize(stringReader);
        }
    }
    

    I tried to add [XmlElement] twice in the field, one per element name, but that didn't work.

  • Luiggi Mendoza
    Luiggi Mendoza almost 10 years
    While this may work, I have several fields and classes to implement such behavior, so I don't think this is a solution for my current problem.
  • Barak Itkin
    Barak Itkin almost 10 years
    I see (I did say it's ugly :P), will try to find a better one
  • dbc
    dbc almost 10 years
    If you need to serialize the data back out, you can add a boolean method ShouldSerializeLeParentId to suppress writing "LeParentId". Or you could even remember which was read and then serialize out in the appropriate language.
  • Luiggi Mendoza
    Luiggi Mendoza almost 10 years
    @dbc that seems to work for SharePoint, which is not my case =\
  • dbc
    dbc almost 10 years
    @Luiggi Mendoza - I have used it successfully with XmlSerializer (to serialize settings and suppress output of nullable booleans) in the past (c# 3.0 era). This post from 2011 indicates that it still works.
  • dbc
    dbc almost 10 years
    @Luiggi Mendoza - and an example from 2013 showing usage with "XmlAttribute": pooyakhamooshi.blogspot.com/2013/04/…
  • kwitee
    kwitee over 4 years
    Just a note: you don't need to derive from the XmlSerializer to handle unknown elements. So there is no need to change your serialization class.
  • Matt P
    Matt P almost 4 years
    I love this solution. Can confirm this does work with inherited properties and extending it for attributes is very simple.