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

c# - Serializing an array of multiple types using XmlSerializer

I'm trying to use XMLSerializer to generate XML such as the following, where the contents of <create> is an array, but the elements can be of differing types (in this case <vendor>, <customer>, and <asset>). Is this possible?

...
    <create>
        <vendor> 
            <vendorid>Unit - A-1212</vendorid>
            <name>this is the name8</name>
            <vcf_bill_siteid3>FOOBAR8</vcf_bill_siteid3>
        </vendor>             
        <customer>
            <CUSTOMERID>XML121</CUSTOMERID>
            <NAME>XML Customer 111</NAME>
        </customer>             
        <asset>  
            <createdAt>San Jose</createdAt>
            <createdBy>Kevin</createdBy>
            <serial_number>123456789</serial_number>
        </asset> 
    </create>
....
See Question&Answers more detail:os

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

1 Reply

0 votes
by (71.8m points)

Assuming all possible types in the array are known at compile time, you can apply multiple [XmlArrayItem(String, Type)] attributes to the array for each known type that could appear in the array. The Type argument is a specific derived type that could appear in the array while the String argument is the element name to associate with that type. Also apply the [XmlArray(String)] attribute to the overall array property to specify the name of the array and that it is to be serialized in two levels rather than one.

E.g.:

public class Document
{
    [XmlArray("create")]
    [XmlArrayItem("vendor", typeof(Vendor))]
    [XmlArrayItem("customer", typeof(Customer))]
    [XmlArrayItem("asset", typeof(Asset))]
    public CreateBase [] Create { get; set; }
}

Where

public abstract class CreateBase
{
}

public class Vendor : CreateBase
{
    public string vendorid { get; set; }
    public string name { get; set; }
    public string vcf_bill_siteid3 { get; set; }
}

public class Customer : CreateBase
{
    public string CUSTOMERID { get; set; }
    public string NAME { get; set; }
}

public class Asset : CreateBase
{
    public string createdAt { get; set; }
    public string createdBy { get; set; }
    public string serial_number { get; set; }
}

(Using an abstract base type is just my preference. You could use object as you base type: public object [] Create { get; set; })

Update

Serializing polymorphic collections containing derived types not known at compile time is difficult with XmlSerializer because it works through dynamic code generation. I.e. when you create an XmlSerializer for the first time it uses reflection to write c# code to serialize and deserialize all statically discoverable referenced types, then compiles and loads that code into a dynamic DLL to do the actual work. No code gets created for types that cannot be discovered statically, so (de)serialization will fail for them.

You have two options to work around this limitation:

  1. Discover all derived types in the list at runtime, then construct an XmlAttributeOverrides, add XmlAttributes for the polymorphic array property, then fill in the XmlArrayItems for the array property with the discovered subtypes. Then pass the XmlAttributeOverrides to the appropriate XmlSerializer constructor.

    Note - you must cache and reuse the XmlSerializer in an appropriate hash table or you will have an enormous resource leak. See here.

    For an example of how this might be done, see here: Force XML serialization of XmlDefaultValue values.

  2. Discover all derived types in the list at runtime, then store then in a custom List<T> subclass that implements IXmlSerializable.

Due to the nuisance of having to cache the XmlSerializer, I lean towards the second approach.

To discover all derived types:

public static class TypeExtensions
{
    public static IEnumerable<Type> DerivedTypes(this IEnumerable<Type> baseTypes)
    {
        var assemblies = baseTypes.SelectMany(t => t.Assembly.GetReferencingAssembliesAndSelf()).Distinct();
        return assemblies
            .SelectMany(a => a.GetTypes())
            .Where(t => baseTypes.Any(baseType => baseType.IsAssignableFrom(t)))
            .Distinct();
    }
}

public static class AssemblyExtensions
{
    public static IEnumerable<Assembly> GetAllAssemblies()
    {
        // Adapted from 
        // https://stackoverflow.com/questions/851248/c-sharp-reflection-get-all-active-assemblies-in-a-solution
        return Assembly.GetEntryAssembly().GetAllReferencedAssemblies();
    }

    public static IEnumerable<Assembly> GetAllReferencedAssemblies(this Assembly root)
    {
        // WARNING: Assembly.GetAllReferencedAssemblies() will optimize away any reference if there
        // is not an explicit use of a type in that assembly from the referring assembly --
        // And simply adding an attribute like [XmlInclude(typeof(T))] seems not to do
        // the trick.  See
        // https://social.msdn.microsoft.com/Forums/vstudio/en-US/17f89058-5780-48c5-a43a-dbb4edab43ed/getreferencedassemblies-not-returning-complete-list?forum=netfxbcl
        // Thus if you are using this to, say, discover all derived types of a base type, the assembly
        // of the derived types MUST contain at least one type that is referenced explicitly from the 
        // root assembly, directly or indirectly.

        var list = new HashSet<string>();
        var stack = new Stack<Assembly>();

        stack.Push(root);

        do
        {
            var asm = stack.Pop();

            yield return asm;

            foreach (var reference in asm.GetReferencedAssemblies())
                if (!list.Contains(reference.FullName))
                {
                    stack.Push(Assembly.Load(reference));
                    list.Add(reference.FullName);
                }

        }
        while (stack.Count > 0);
    }

    public static IEnumerable<Assembly> GetReferencingAssemblies(this Assembly target)
    {
        if (target == null)
            throw new ArgumentNullException();
        // Assemblies can have circular references:
        // http://stackoverflow.com/questions/1316518/how-did-microsoft-create-assemblies-that-have-circular-references
        // So a naive algorithm isn't going to work.

        var done = new HashSet<Assembly>();

        var root = Assembly.GetEntryAssembly();
        var allAssemblies = root.GetAllReferencedAssemblies().ToList();

        foreach (var assembly in GetAllAssemblies())
        {
            if (target == assembly)
                continue;
            if (done.Contains(assembly))
                continue;
            var refersTo = (assembly == root ? allAssemblies : assembly.GetAllReferencedAssemblies()).Contains(target);
            done.Add(assembly);
            if (refersTo)
                yield return assembly;
        }
    }

    public static IEnumerable<Assembly> GetReferencingAssembliesAndSelf(this Assembly target)
    {
        return new[] { target }.Concat(target.GetReferencingAssemblies());
    }
}

Then, the polymorphic list that discovers its own types:

public class XmlPolymorphicList<T> : List<T>, IXmlSerializable where T : class
{
    static XmlPolymorphicList()
    {
        // Make sure the scope of objects to find isn't *EVERYTHING*
        if (typeof(T) == typeof(object))
        {
            throw new InvalidOperationException("Cannot create a XmlPolymorphicList<object>");
        }
    }

    internal sealed class DerivedTypeDictionary
    {
        Dictionary<Type, string> derivedTypeNames;
        Dictionary<string, Type> derivedTypes;

        DerivedTypeDictionary()
        {
            derivedTypeNames = typeof(T).DerivedTypes().ToDictionary(t => t, t => t.DefaultXmlElementName());
            derivedTypes = derivedTypeNames.ToDictionary(p => p.Value, p => p.Key); // Will throw an exception if names are not unique
        }

        public static DerivedTypeDictionary Instance { get { return Singleton<DerivedTypeDictionary>.Instance; } }

        public string GetName(Type type)
        {
            return derivedTypeNames[type];
        }

        public Type GetType(string name)
        {
            return derivedTypes[name];
        }
    }

    public XmlPolymorphicList()
        : base()
    {
    }

    public XmlPolymorphicList(IEnumerable<T> items)
        : base(items)
    {
    }

    #region IXmlSerializable Members

    XmlSchema IXmlSerializable.GetSchema()
    {
        return null;
    }

    void IXmlSerializable.ReadXml(XmlReader reader)
    {
        reader.ReadStartElement();
        while (reader.NodeType == XmlNodeType.Element)
        {
            var name = reader.Name;
            var type = DerivedTypeDictionary.Instance.GetType(name);
            var item = (T)(new XmlSerializer(type).Deserialize(reader));
            if (item != null)
                Add(item);
        }
    }

    void IXmlSerializable.WriteXml(XmlWriter writer)
    {
        foreach (var item in this)
        {
            new XmlSerializer(item.GetType()).Serialize(writer, item);
        }
    }

    #endregion
}

public static class XmlSerializationHelper
{
    public static string DefaultXmlElementName(this Type type)
    {
        var xmlType = type.GetCustomAttribute<XmlTypeAttribute>();
        if (xmlType != null && !string.IsNullOrEmpty(xmlType.TypeName))
            return xmlType.TypeName;
        return type.Name;
    }
}

public class Singleton<T> where T : class
{
    // Explicit static constructor to tell C# compiler
    // not to mark type as beforefieldinit
    static Singleton()
    {
    }

    /// <summary>
    /// Private nested class which acts as singleton class instantiator. This class should not be accessible outside <see cref="Singleton<T>"/>
    /// </summary>
    class Nested
    {
        /// <summary>
        /// Explicit static constructor to tell C# compiler not to mark type as beforefieldinit
        /// </summary>
        static Nested()
        {
        }

        /// <summary>
        /// Static instance variable
        /// </summary>
        internal static readonly T instance = (T)Activator.CreateInstance(typeof(T), true);
    }

    public static T Instance { get { return Nested.instance; } }
}

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

...