简体   繁体   中英

Better IXmlSerializable format?

I have an interface IInput that is preventing the XmlSerializer from serializing the class natively (because it doesnt like interfaces). I found a hack/workaround that attempts to just create the underlying implementation when deserializing and then casts that back to the interface. The deserializer knows the underlying implementation because its encoded as a attribute AssemblyQualifiedName

In order to take advantage of this technique I have to implement IXmlSerializable, but only 1 property really needs help ( IInput Input ), I wish for all the other ones to act as if they were normal. Here is my class, it works as expected but seems like a very messy way of getting types of which the normal XMLserializer can serialize to conform to an IXmlSerialiable interface.

Is there some sort of "Serialize all properties natively except x"? If not what is one way I can make this more readable and/or less copy and pasted

public class JobInput : IJobInput, IXmlSerializable
    {
        public int AgencyId { get; set; }
        public Guid ExternalId { get; set; }
        public string Requester { get; set; }
        public IInput Input { get; set; }

        public XmlSchema GetSchema()
        {
            return null;
        }

        public void ReadXml(XmlReader reader)
        {
            reader.MoveToContent();
            reader.ReadStartElement();

            if (!reader.IsEmptyElement) 
            {
                reader.ReadStartElement("AgencyId");
                var xmlSerializer = new XmlSerializer(AgencyId.GetType());
                AgencyId = ((int)xmlSerializer.Deserialize(reader));
                reader.ReadEndElement();

                reader.ReadStartElement("ExternalId");
                xmlSerializer = new XmlSerializer(ExternalId.GetType());
                ExternalId = ((Guid)xmlSerializer.Deserialize(reader));
                reader.ReadEndElement();

                reader.ReadStartElement("Requester");
                xmlSerializer = new XmlSerializer(typeof(string));
                Requester = ((string)xmlSerializer.Deserialize(reader));
                reader.ReadEndElement();

                var type = Type.GetType(reader.GetAttribute("AssemblyQualifiedName"), true);
                reader.ReadStartElement("IInput");
                xmlSerializer = new XmlSerializer(type);
                Input = ((IInput)xmlSerializer.Deserialize(reader));
                reader.ReadEndElement();
                reader.ReadEndElement();
            }
        }

        public void WriteXml(XmlWriter writer)
        {
            writer.WriteStartElement("AgencyId");
            var xmlSerializer = new XmlSerializer(AgencyId.GetType());
            xmlSerializer.Serialize(writer, AgencyId);
            writer.WriteEndElement();

            writer.WriteStartElement("ExternalId");
            xmlSerializer = new XmlSerializer(ExternalId.GetType());
            xmlSerializer.Serialize(writer, ExternalId);
            writer.WriteEndElement();

            writer.WriteStartElement("Requester");
            xmlSerializer = new XmlSerializer(Requester.GetType());
            xmlSerializer.Serialize(writer, Requester);
            writer.WriteEndElement();


            writer.WriteStartElement("IInput");
            writer.WriteAttributeString("AssemblyQualifiedName", Input.GetType().AssemblyQualifiedName);
            xmlSerializer = new XmlSerializer(Input.GetType());
            xmlSerializer.Serialize(writer, Input);
            writer.WriteEndElement();
        }
    }

Is it possible to have a generic function that can just detect the type for all the concrete types and serialize/deserialize appropriately. I would like something like

public void WriteXml(XmlWriter writer) {
    GenericSerialize("AgencyId", AgencyId, writer);
    GenericSerialize("ExternalId", ExternalId, writer);
    GenericSerialize("Requester", Requester, writer);

    writer.WriteStartElement("IInput");
    writer.WriteAttributeString("AssemblyQualifiedName", Input.GetType().AssemblyQualifiedName);
    xmlSerializer = new XmlSerializer(Input.GetType());
    xmlSerializer.Serialize(writer, Input);
    writer.WriteEndElement();
}

You can use [XmlAnyElement] to add an XElement [] -valued property to your class that handles serialization and deserialization of properties that cannot be automatically serialized, like so:

[XmlRoot(Namespace = JobInput.XmlNamespace)]
[XmlType(Namespace = JobInput.XmlNamespace)]
public class JobInput
{
    const string XmlNamespace = "";

    public int AgencyId { get; set; }
    public Guid ExternalId { get; set; }
    public string Requester { get; set; }

    [XmlIgnore]
    public IInput Input { get; set; }

    [XmlAnyElement]
    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public XElement[] XmlCustomElements
    {
        get
        {
            var list = new List<XElement>();
            if (Input != null)
                list.Add(Input.SerializePolymorphicToXElement(XName.Get("Input", XmlNamespace)));
            // Add others as needed.
            return list.ToArray();
        }
        set
        {
            if (value == null)
                return;
            this.Input = value.DeserializePolymorphicEntry<IInput>(XName.Get("Input", XmlNamespace));
            // Add others as needed.
        }
    }
}

Your standard properties will now get automatically serialized and your custom properties can be semi-automatically serialized through nested calls to XmlSerializer using the appropriate type. The following extension methods are required:

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

    public static object Deserialize(this XContainer element, Type type, XmlSerializer serializer)
    {
        using (var reader = element.CreateReader())
        {
            return (serializer ?? new XmlSerializer(type)).Deserialize(reader);
        }
    }

    public static XElement SerializeToXElement<T>(this T obj, XmlSerializerNamespaces ns, XmlSerializer serializer)
    {
        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;
    }

    const string TypeAttributeName = "AssemblyQualifiedName";

    public static T DeserializePolymorphicEntry<T>(this XElement[] arrayValue, XName name)
    {
        var element = arrayValue.Where(e => e.Name == name).FirstOrDefault();
        return element.DeserializePolymorphic<T>(name);
    }

    public static T DeserializePolymorphic<T>(this XElement value, XName name)
    {
        if (value == null)
            return default(T);
        var typeName = (string)value.Attribute(TypeAttributeName);
        if (typeName == null)
            throw new InvalidOperationException(string.Format("Missing AssemblyQualifiedName for \"{0}\"", value.ToString()));
        var type = Type.GetType(typeName, true); // Throw on error
        return (T)value.Deserialize(type, XmlSerializerFactory.Create(type, name));
    }

    public static XElement SerializePolymorphicToXElement<T>(this T obj, XName name)
    {
        if (obj == null)
            return null;
        var element = obj.SerializeToXElement(XObjectExtensions.NoStandardXmlNamespaces(), XmlSerializerFactory.Create(obj.GetType(), name));
        // Remove namespace attributes (they will be added back by the XmlWriter if needed)
        foreach (var attr in element.Attributes().Where(a => a.IsNamespaceDeclaration).ToList())
            attr.Remove();
        element.Add(new XAttribute("AssemblyQualifiedName", obj.GetType().AssemblyQualifiedName));
        return element;
    }
}

public static class XmlSerializerFactory
{
    static readonly object padlock;
    static readonly Dictionary<Tuple<Type, XName>, XmlSerializer> serializers;

    // An explicit static constructor enables fairly lazy initialization.
    static XmlSerializerFactory()
    {
        padlock = new object();
        serializers = new Dictionary<Tuple<Type, XName>, XmlSerializer>();
    }

    /// <summary>
    /// Return a cached XmlSerializer for the given type and root name.
    /// </summary>
    /// <param name="type"></param>
    /// <param name="name"></param>
    /// <returns>a cached XmlSerializer</returns>
    public static XmlSerializer Create(Type type, XName name)
    {
        // According to https://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlserializer%28v=vs.110%29.aspx
        // XmlSerializers created with new XmlSerializer(Type, XmlRootAttribute) must be cached in a hash table 
        // to avoid a severe memory leak & performance hit.
        if (type == null)
            throw new ArgumentNullException();
        if (name == null)
            return new XmlSerializer(type);
        lock (padlock)
        {
            XmlSerializer serializer;
            var key = Tuple.Create(type, name);
            if (!serializers.TryGetValue(key, out serializer))
                serializers[key] = serializer = new XmlSerializer(type, new XmlRootAttribute { ElementName = name.LocalName, Namespace = name.NamespaceName });
            return serializer;
        }
    }
}

Doing it this way makes your class look simpler and reduces the possibility of bugs from mistakes in implementing IXmlSerializable - but it does require a little bit of reusable infrastructure.

Prototype fiddle .

Seems what I was after was possible

public void GenericWriter<T>(ref XmlWriter writer, string propertyName, T property)
{
    writer.WriteStartElement(propertyName);
    var xmlSerializer = new XmlSerializer(typeof(T));
    xmlSerializer.Serialize(writer, property);
    writer.WriteEndElement();
}

so same for reader. I ended up using ref just in case.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM