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
560 views
in Technique[技术] by (71.8m points)

c# - Deserializing XML with unknown element order

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?

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 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:

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

(Note -- only partially tested.)

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.


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

...