2

I'm trying to implement a client for a service with a really deficient spec. It's SOAP-like, although it has no WSDL or equivalent file. The spec also doesn't provide any information about the correct ordering of elements - they're listed alphabetically in the spec, but the service returns an XML parse error if they're out of order in the request (said order to be derived by examining the examples).

I can work with this for submitting requests, even if it's a pain. However, I don't know how to handle responses correctly.

With both SoapEnvelope and directly with XmlSerializer, if the response contains an element I haven't yet ordered correctly, it shows up as null on my object. Once again, I can manage to work with this, and manually order the class properties with Order attributes, but I have no way to tell whether the original XML has a field that I didn't order correctly and thus got left as null.

This leads me to the current question: How can I check if the XmlSerializer dropped a field?

dbc
  • 104,963
  • 20
  • 228
  • 340
Bobson
  • 13,498
  • 5
  • 55
  • 80
  • I'm really hoping this is actually an X-Y problem, and there's a simpler answer than checking up on the XmlSerializer, but I'm not expecting one. – Bobson Nov 03 '15 at 18:51
  • 2
    You can use [`XmlSerializer.UnknownElement`](https://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlserializer.unknownelement%28v=vs.110%29.aspx). – dbc Nov 03 '15 at 18:59
  • @dbc - Oooh, that could work. It's not ideal, but I can definitely work with it. Of course, nothing about this situation is ideal, so I shouldn't be surprised. – Bobson Nov 03 '15 at 19:06
  • @dbc - Actually, before you go and turn that into an answer, is there a way to do that as part of the `SoapEnvelope.GetBodyObject()` call, or do I need to use the XmlSerializer directly? – Bobson Nov 03 '15 at 19:12
  • Just to be sure I understand: when serializing, you want to set [`XmlElementAttribute.Order`](https://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlelementattribute.order%28v=vs.110%29.aspx) or [`XmlArrayAttribute.Order`](https://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlarrayattribute.order%28v=vs.110%29.aspx) for each property in your types, but when deserializing, you want to ignore the order and deserialize elements in any order? – dbc Nov 03 '15 at 19:12
  • @dbc - Yes, that would be best. Alternatively, I need to at least log a warning somewhere that I ignored a field (which is what UnknownElement helps with). Because I only know the correct ordering of a subset of all possible fields in the reply. So I can manually specify the order for those fields, but I need to know that I need to go add support for any fields that I missed. – Bobson Nov 03 '15 at 19:14
  • By `SoapEnvelope.GetBodyObject()` do you mean [this](https://msdn.microsoft.com/en-us/library/aa748457.aspx) from [Web Service Enhancements 3.0](https://msdn.microsoft.com/en-us/library/aa139626.aspx)? I've never used that before. – dbc Nov 03 '15 at 19:26
  • @dbc - Yes. I'm only using it for that class, so I forgot it wasn't part of the standard set of .NET libraries. It's fine if you don't have an answer that uses that. – Bobson Nov 03 '15 at 19:28

1 Answers1

7

You can use the XmlSerializer.UnknownElement event on XmlSerializer to capture out-of-order elements. This will allow you to manually find and fix problems in deserialization.

A more complex answer would be to correctly order your elements when serializing, but ignore order when deserializing. This requires using the XmlAttributes class and the XmlSerializer(Type, XmlAttributeOverrides) constructor. Note that serializers constructed in this manner must be cached in a hash table and resused to avoid a severe memory leak, and thus this solution is a little "finicky" since Microsoft doesn't provide a meaningful GetHashCode() for XmlAttributeOverrides. The following is one possible implementation which depends upon knowing in advance all types that need their XmlElementAttribute.Order and XmlArrayAttribute.Order properties ignored, thus avoiding the need to create a complex custom hashing method:

 // https://stackoverflow.com/questions/33506708/deserializing-xml-with-unknown-element-order
public class XmlSerializerFactory : XmlOrderFreeSerializerFactory
{
    static readonly XmlSerializerFactory instance;

    // Use a static constructor for lazy initialization.
    private XmlSerializerFactory()
        : base(new[] { typeof(Type2), typeof(Type1), typeof(TestClass), typeof(Type3) }) // These are the types in your client for which Order needs to be ignored whend deserializing
    {
    }

    static XmlSerializerFactory()
    {
        instance = new XmlSerializerFactory();
    }

    public static XmlSerializerFactory Instance { get { return instance; } }
}

public abstract class XmlOrderFreeSerializerFactory
{
    readonly XmlAttributeOverrides overrides;
    readonly object locker = new object();
    readonly Dictionary<Type, XmlSerializer> serializers = new Dictionary<Type, XmlSerializer>();

    static void AddOverrideAttributes(Type type, XmlAttributeOverrides overrides)
    {
        if (type == null || type == typeof(object) || type.IsPrimitive || type == typeof(string))
            return;

        var mask = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public;
        foreach (var member in type.GetProperties(mask).Cast<MemberInfo>().Union(type.GetFields(mask)))
        {
            XmlAttributes overrideAttr = null;
            foreach (var attr in member.GetCustomAttributes<XmlElementAttribute>())
            {
                overrideAttr = overrideAttr ?? new XmlAttributes();
                overrideAttr.XmlElements.Add(new XmlElementAttribute { DataType = attr.DataType, ElementName = attr.ElementName, Form = attr.Form, IsNullable = attr.IsNullable, Namespace = attr.Namespace, Type = attr.Type });
            }
            foreach (var attr in member.GetCustomAttributes<XmlArrayAttribute>())
            {
                overrideAttr = overrideAttr ?? new XmlAttributes();
                overrideAttr.XmlArray = new XmlArrayAttribute { ElementName = attr.ElementName, Form = attr.Form, IsNullable = attr.IsNullable, Namespace = attr.Namespace };
            }
            foreach (var attr in member.GetCustomAttributes<XmlArrayItemAttribute>())
            {
                overrideAttr = overrideAttr ?? new XmlAttributes();
                overrideAttr.XmlArrayItems.Add(attr);
            }
            foreach (var attr in member.GetCustomAttributes<XmlAnyElementAttribute>())
            {
                overrideAttr = overrideAttr ?? new XmlAttributes();
                overrideAttr.XmlAnyElements.Add(new XmlAnyElementAttribute { Name = attr.Name, Namespace = attr.Namespace });
            }
            if (overrideAttr != null)
                overrides.Add(type, member.Name, overrideAttr);
        }
    }

    protected XmlOrderFreeSerializerFactory(IEnumerable<Type> types)
    {
        overrides = new XmlAttributeOverrides();
        foreach (var type in types.SelectMany(t => t.BaseTypesAndSelf()).Distinct())
        {
            AddOverrideAttributes(type, overrides);
        }
    }

    public XmlSerializer GetSerializer(Type type)
    {
        if (type == null)
            throw new ArgumentNullException("type");
        lock (locker)
        {
            XmlSerializer serializer;
            if (!serializers.TryGetValue(type, out serializer))
                serializers[type] = serializer = new XmlSerializer(type, overrides);
            return serializer;
        }
    }
}

public static class TypeExtensions
{
    public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
    {
        while (type != null)
        {
            yield return type;
            type = type.BaseType;
        }
    }
}

Then when deserializing a type, use the XmlSerializer provided by the factory. Given that SoapEnvelope is a subclass of XmlDocument, you should be able to deserialize the body node along the lines of the answer in Deserialize object property with StringReader vs XmlNodeReader.

Note -- only moderately tested. Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340