简体   繁体   中英

Serialize/Deserialize derived class as base class

For example I have the following classes:

public abstract class Device
{
}

public class WindowsDevice: Device
{
}

public class AndroidDevice: Device
{
}

Now I want to serialize/deserialize WindowsDevice and AndroidDevice as XML:

public static string Serialize(object o, Type[] additionalTypes = null)
    {
        var serializer = new XmlSerializer(o.GetType(), additionalTypes);

        using (var stringWriter = new StringWriterWithEncoding(Encoding.UTF8))
        {
            serializer.Serialize(stringWriter, o);
            return stringWriter.ToString();
        }
    }

This will produce the following output:

<?xml version="1.0" encoding="utf-8"?>
<WindowsDevice xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">

</WindowsDevice>

But now I am unable to deserialize this, because in my application I don't know if the XML is WindowsDevice or AndroidDevice, so I have to deserialize as typeof(Device). But then I will get an exception that "WindowsDevice" was unexpected in the XML.

I tried XmlInclude and extraTypes without any success.

What I dont understand is, that if I have the following sample class:

public class SampleClass
{
    public List<Device> Devices {get;set}
}

and if I serialize SampleClass and use XmlInclude or extraTypes I exactly get what I want:

<Devices>
<Device xsi:type="WindowsDevice"></Device>
</Devices>

But I don't have that class and I don't have a list of Devices. I only want to serialize/deserialize WindowsDevice and AndroidDevice but on Deserialize I don't know whether it is AndroidDevice or WindowsDevice so I have to use typeof(Device) and want to get the correct sublass AndroidDevice or WindowsDevice, so instead of:

<WindowsDevice></WindowsDevice>

I want to have:

<Device xsi:type="WindowsDevice"></Device>

How can this be done?

Your problem is that you are constructing your XmlSerializer inconsistently during serialization and deserialization. You need to construct it using the same Type argument in both cases, specifically the base type typeof(Device) . Thus I'd suggest you replace your existing completely general serialization method with one specific for a Device :

public static class DeviceExtensions
{
    public static string SerializeDevice<TDevice>(this TDevice o) where TDevice : Device
    {
        // Ensure that [XmlInclude(typeof(TDevice))] is present on Device.
        // (Included for clarity -- actually XmlSerializer will make a similar check.)
        if (!typeof(Device).GetCustomAttributes<XmlIncludeAttribute>().Any(a => a.Type == o.GetType()))
        {
            throw new InvalidOperationException("Unknown device type " + o.GetType());
        }
        var serializer = new XmlSerializer(typeof(Device)); // Serialize as the base class
        using (var stringWriter = new StringWriterWithEncoding(Encoding.UTF8))
        {
            serializer.Serialize(stringWriter, o);
            return stringWriter.ToString();
        }
    }

    public static Device DeserializeDevice(this string xml)
    {
        var serial = new XmlSerializer(typeof(Device));
        using (var reader = new StringReader(xml))
        {
            return (Device)serial.Deserialize(reader);
        }
    }
}

Then, apply [XmlInclude(typeof(TDevice))] to Device for all possible subtypes:

[XmlInclude(typeof(WindowsDevice))]
[XmlInclude(typeof(AndroidDevice))]
public abstract class Device
{
}

Then both types of devices can now be serialized and deserialized successfully while retaining their type, because XmlSerializer will include an "xsi:type" attribute to explicitly indicate the type:

<Device xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:type="WindowsDevice" />

Or

<Device xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:type="AndroidDevice" />

Sample fiddle .

Updates

So the issue was, that I serialized with typeof(WindowsDevice) instead of typeof(Device)?

Yes.

Any ideas for a solution which will work, if I have to use typeof(WindowsDevice)? Cause I have hundreds of classes and don't want to use hundreds of different XmlSerializer initializations...

This is more of an architectural question than a howto question. One possibility would be to introduce a custom attribute that you can apply to a class to indicate that any subtypes of that class should always be serialized as the attributed base type. All appropriate [XmlInclude(typeof(TDerivedType))] attributes will also be required:

[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class XmlBaseTypeAttribute : System.Attribute
{
}   

[XmlInclude(typeof(WindowsDevice))]
[XmlInclude(typeof(AndroidDevice))]
[XmlBaseType]
public abstract class Device
{
}

Then modify your universal XML serialization code to look up the type hierarchy of the object being serialized for an [XmlBaseType] attribute, and (de)serialize as that type:

public static class XmlExtensions
{
    static Type GetSerializedType(this Type type)
    {
        var serializedType = type.BaseTypesAndSelf().Where(t => Attribute.IsDefined(t, typeof(XmlBaseTypeAttribute))).SingleOrDefault();
        if (serializedType != null)
        {
            // Ensure that [XmlInclude(typeof(TDerived))] is present on the base type
            // (Included for clarity -- actually XmlSerializer will make a similar check.)
            if (!serializedType.GetCustomAttributes<XmlIncludeAttribute>().Any(a => a.Type == type))
            {
                throw new InvalidOperationException(string.Format("Unknown subtype {0} of type {1}", type, serializedType));
            }
        }
        return serializedType ?? type;
    }

    public static string Serialize(this object o)
    {
        var serializer = new XmlSerializer(o.GetType().GetSerializedType());
        using (var stringWriter = new StringWriterWithEncoding(Encoding.UTF8))
        {
            serializer.Serialize(stringWriter, o);
            return stringWriter.ToString();
        }
    }

    public static T Deserialize<T>(this string xml)
    {
        var serial = new XmlSerializer(typeof(T).GetSerializedType());
        using (var reader = new StringReader(xml))
        {
            return (T)serial.Deserialize(reader);
        }
    }
}

Of course this means that if your code tries to deserialize XML it expects to contain a WindowsDevice , it might actually get back an AndroidDevice depending upon the contents of the XML.

Sample fiddle #2 .

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