Сериализация и восстановление неизвестного класса

Базовый проект содержит абстрактный базовый класс Foo. В отдельных клиентских проектах есть классы, реализующие этот базовый класс.

Я хотел бы сериализовать и восстановить экземпляр конкретного класса, вызвав некоторый метод базового класса:

// In the base project:
public abstract class Foo
{
    abstract void Save (string path);
    abstract Foo Load (string path);
}

Можно предположить, что на момент десериализации все необходимые классы присутствуют. Если возможно каким-либо образом, сериализацию следует выполнять в XML. Возможно создание базового класса для реализации IXmlSerializable.

Я немного застрял здесь. Если я правильно понимаю, то это возможно только путем добавления [XmlInclude(typeof(UnknownClass))] к базовому классу для каждого реализующего класса, но реализующие классы неизвестны!

Есть ли способ сделать это? У меня нет опыта рефлексии, но я также приветствую ответы, использующие его.

Изменить: проблема в де- сериализации. Просто сериализация была бы легкой. :-)

Ответов (9)

Решение

Вы также можете сделать это в момент создания XmlSerializer, указав дополнительные сведения в конструкторе. Обратите внимание, что он не использует такие модели повторно, поэтому вам нужно настроить XmlSerializer один раз (при запуске приложения, из конфигурации) и повторно использовать его многократно ... обратите внимание, что с XmlAttributeOverrides перегрузкой возможно гораздо больше настроек . .

using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
static class Program
{
    static readonly XmlSerializer ser;
    static Program()
    {
        List<Type> extraTypes = new List<Type>();
        // TODO: read config, or use reflection to
        // look at all assemblies
        extraTypes.Add(typeof(Bar));
        ser = new XmlSerializer(typeof(Foo), extraTypes.ToArray());
    }
    static void Main()
    {
        Foo foo = new Bar();
        MemoryStream ms = new MemoryStream();
        ser.Serialize(ms, foo);
        ms.Position = 0;
        Foo clone = (Foo)ser.Deserialize(ms);
        Console.WriteLine(clone.GetType());
    }
}

public abstract class Foo { }
public class Bar : Foo {}

Этот метод считывает корневой элемент XML и проверяет, содержит ли текущая исполняемая сборка тип с таким именем. Если это так, XML-документ десериализуется. В противном случае выдается ошибка.

public static T FromXml<T>(string xmlString)
{
    Type sourceType;
    using (var stringReader = new StringReader(xmlString))
    {
        var rootNodeName = XElement.Load(stringReader).Name.LocalName;
        sourceType =
                Assembly.GetExecutingAssembly().GetTypes()
                    .FirstOrDefault(t => t.IsSubclassOf(typeof(T)) 
                                    && t.Name == rootNodeName)
                ??
                Assembly.GetAssembly(typeof(T)).GetTypes()
                    .FirstOrDefault(t => t.IsSubclassOf(typeof(T)) 
                                    && t.Name == rootNodeName);

        if (sourceType == null)
        {
            throw new Exception();
        }
    }

    using (var stringReader = new StringReader(xmlString))
    {
        if (sourceType.IsSubclassOf(typeof(T)) || sourceType == typeof(T))
        {
            var ser = new XmlSerializer(sourceType);

            using (var xmlReader = new XmlTextReader(stringReader))
            {
                T obj;
                obj = (T)ser.Deserialize(xmlReader);
                xmlReader.Close();
                return obj;
            }
        }
        else
        {
            throw new InvalidCastException(sourceType.FullName
                                           + " cannot be cast to "
                                           + typeof(T).FullName);
        }
    }
}

Пометка классов как Serializable и использование Soap BinaryFormatter вместо XmlSerializer автоматически предоставит вам эту функциональность. При сериализации информация о типе сериализуемого экземпляра будет записана в XML, и Soap BinaryFormatter может создавать экземпляры подклассов при десериализации.

Я использовал атрибут XmlType неизвестных (но ожидаемых) классов, чтобы определить Тип для десериализации. Ожидаемые типы загружаются во время создания экземпляра класса AbstractXmlSerializer и помещаются в словарь. Во время десериализации корневой элемент считывается, и при этом тип извлекается из словаря. После этого его можно десериализовать обычным образом.

XmlMessage.class:

public abstract class XmlMessage
{
}

IdleMessage.class:

[XmlType("idle")]
public class IdleMessage : XmlMessage
{
    [XmlElement(ElementName = "id", IsNullable = true)]
    public string MessageId
    {
        get;
        set;
    }
}

AbstractXmlSerializer.class:

public class AbstractXmlSerializer<AbstractType> where AbstractType : class
{
    private Dictionary<String, Type> typeMap;

    public AbstractXmlSerializer(List<Type> types)
    {            
        typeMap = new Dictionary<string, Type>();

        foreach (Type type in types)
        {
            if (type.IsSubclassOf(typeof(AbstractType))) {
                object[] attributes = type.GetCustomAttributes(typeof(XmlTypeAttribute), false);

                if (attributes != null && attributes.Count() > 0)
                {
                    XmlTypeAttribute attribute = attributes[0] as XmlTypeAttribute;
                    typeMap[attribute.TypeName] = type;
                }
            }
        }
    }

    public AbstractType Deserialize(String xmlData)
    {
        if (string.IsNullOrEmpty(xmlData))
        {
            throw new ArgumentException("xmlData parameter must contain xml");
        }            

        // Read the Data, Deserializing based on the (now known) concrete type.
        using (StringReader stringReader = new StringReader(xmlData))
        {
            using (XmlReader xmlReader = XmlReader.Create(stringReader))
            {
                String targetType = GetRootElementName(xmlReader);

                if (targetType == null)
                {
                    throw new InvalidOperationException("XML root element was not found");
                }                        

                AbstractType result = (AbstractType)new
                    XmlSerializer(typeMap[targetType]).Deserialize(xmlReader);
                return result;
            }
        }
    }

    private static string GetRootElementName(XmlReader xmlReader)
    {            
        if (xmlReader.IsStartElement())
        {
            return xmlReader.Name;
        }

        return null;
    }
}

Модульный тест:

[TestMethod]
public void TestMethod1()
{
    List<Type> extraTypes = new List<Type>();
    extraTypes.Add(typeof(IdleMessage));
    AbstractXmlSerializer<XmlMessage> ser = new AbstractXmlSerializer<XmlMessage>(extraTypes);

    String xmlMsg = "<idle></idle>";

    MutcMessage result = ser.Deserialize(xmlMsg);
    Assert.IsTrue(result is IdleMessage);           
}

Эти ссылки, вероятно, будут вам полезны:

У меня сложный проект удаленного взаимодействия, и мне нужен очень жесткий контроль над сериализованным XML. Сервер мог получать объекты, которые он не знал, как десериализовать, и наоборот, поэтому мне нужен был способ их быстрой идентификации.

Все решения .NET, которые я пробовал, не обладали необходимой гибкостью для моего проекта.

Я храню атрибут int в базовом xml для определения типа объекта.

Если мне нужно создать новый объект из xml, я создал фабричный класс, который проверяет атрибут типа, затем создает соответствующий производный класс и передает ему xml.

Я сделал что-то вроде этого (вытащил это из памяти, поэтому синтаксис может быть немного неправильным):

(1) Создал интерфейс

interface ISerialize
{
    string ToXml();
    void FromXml(string xml);       
};

(2) Базовый класс

public class Base : ISerialize
{
    public enum Type
    {
        Base,
        Derived
    };

    public Type m_type;

    public Base()
    {
        m_type = Type.Base;
    }

    public virtual string ToXml()
    {
        string xml;
        // Serialize class Base to XML
        return string;
     }

    public virtual void FromXml(string xml)
    {
        // Update object Base from xml
    }
};

(3) Производный класс

public class Derived : Base, ISerialize
{
    public Derived()
    {
         m_type = Type.Derived;
    }

    public override virtual string ToXml()
    {
        string xml;
        // Serialize class Base to XML
        xml = base.ToXml();
        // Now serialize Derived to XML
        return string;
     }
     public override virtual void FromXml(string xml)
     {
         // Update object Base from xml
         base.FromXml(xml);
         // Update Derived from xml
     }
};

(4) Фабрика объектов

public ObjectFactory
{
    public static Base Create(string xml)
    {
        Base o = null;

        Base.Type t;

        // Extract Base.Type from xml

        switch(t)
        {
            case Base.Type.Derived:
                o = new Derived();
                o.FromXml(xml);
            break;
         }

        return o;
    }
};

Где-то глубоко внутри пространств имен XML находится замечательный класс под названием XmlReflectionImporter.

Это может помочь вам, если вам нужно создать схему во время выполнения.

Что ж, сериализация не должна быть проблемой, конструктор XmlSerializer принимает аргумент Type, даже вызов GetType для экземпляра производного класса через метод на абстрактной основе вернет производные типы фактического типа. Таким образом, по сути, пока вы знаете правильный тип при десериализации, сериализация правильного типа тривиальна. Таким образом, вы можете реализовать метод на базе, называемый сериализацией или что у вас есть, который передает this.GetType() конструктору XmlSerializer .. или просто передает текущую ссылку и позволяет методу сериализации позаботиться об этом, и все будет в порядке.

Изменить: обновление для OP Edit ..

Если вы не знаете тип при десериализации, тогда у вас действительно нет ничего, кроме массива строк или байтов, без какого-либо идентификатора где-то, где вы находитесь как бы в ручье. Есть некоторые вещи, которые вы можете сделать, например, попытаться десериализовать любой известный производный тип базового класса xx, я бы не рекомендовал это.

Другой вариант - пройтись по XML вручную и реконструировать объект, внедрив тип как свойство или что у вас есть, может быть, это то, что вы изначально имели в виду в статье, но в нынешнем виде я не думаю, что есть способ для встроенная сериализация позаботится об этом за вас без указания типа.

Вы также можете сделать это путем создания passign XmlSerializer во всех возможных типов в конструкторе . Имейте в виду, что при использовании этого конструктора xmlSerializer будет компилироваться каждый раз и приведет к утечке, если вы постоянно его воссоздадите. Вы захотите создать единственный сериализатор и повторно использовать его в своем приложении.

Затем вы можете загрузить сериализатор и, используя отражение, искать любых потомков foo.

Вам не нужно помещать функции сериализации в какой-либо базовый класс, вместо этого вы можете добавить их в свой служебный класс.

например (код только для примера, rootName не является обязательным)

public static class Utility
{
       public static void ToXml<T>(T src, string rootName, string fileName) where T : class, new()
        {
            XmlSerializer serializer = new XmlSerializer(typeof(T), new XmlRootAttribute(rootName));
            XmlTextWriter writer = new XmlTextWriter(fileName, Encoding.UTF8);
            serializer.Serialize(writer, src);
            writer.Flush();
            writer.Close();
        }
}

Просто позвоните в

Utility.ToXml( fooObj, "Foo", @"c:\foo.xml");

Его могут использовать не только типы семейства Foo, но и все другие сериализуемые объекты.

РЕДАКТИРОВАТЬ

ОК, полное обслуживание ... (rootName необязательно)

public static T FromXml<T>(T src, string rootName, string fileName) where T : class, new()
{
    XmlSerializer serializer = new XmlSerializer(typeof(T), new XmlRootAttribute(rootName));
    TextReader reader = new StreamReader(fileName);
    return serializer.Deserialize(reader) as T;
}