Serialização XML e tipos herdados

85

Continuando com minha pergunta anterior , tenho trabalhado para que meu modelo de objeto seja serializado em XML. Mas agora encontrei um problema (quelle surpresa!).

O problema que tenho é que tenho uma coleção, que é de um tipo de classe base abstrata, que é preenchida pelos tipos derivados concretos.

Achei que seria ótimo apenas adicionar os atributos XML a todas as classes envolvidas e tudo ficaria bem. Infelizmente, esse não é o caso!

Então, fiz algumas pesquisas no Google e agora entendo por que não está funcionando. Na verdade, o XmlSerializerestá fazendo uma reflexão inteligente para serializar objetos de / para XML e, como é baseado no tipo abstrato, não consegue descobrir com que diabos está falando . Bem.

Eu encontrei esta página no CodeProject, que parece que pode ajudar muito (ainda para ler / consumir totalmente), mas pensei que gostaria de trazer esse problema para a tabela StackOverflow também, para ver se você tem algum hacks / truques para colocá-lo em funcionamento da maneira mais rápida / leve possível.

Uma coisa que também devo acrescentar é que NÃO quero seguir o XmlIncludecaminho. Simplesmente há muito acoplamento com ele, e esta área do sistema está sob intenso desenvolvimento, então seria uma verdadeira dor de cabeça para manutenção!

Rob Cooper
fonte
1
Seria útil ver alguns trechos de código relevantes extraídos das classes que você está tentando serializar.
Rex M de
Mate: Eu reabri porque acho que outras pessoas poderiam achar isso útil, mas sinta-se à vontade para fechar se você discordar
JamesSugrue
Um pouco confuso com isso, já que não houve nada neste tópico por tanto tempo?
Rob Cooper,
Aí está a resposta: stackoverflow.com/questions/6737666/…
Odys

Respostas:

54

Problema resolvido!

OK, então finalmente cheguei lá (admito que com muita ajuda daqui !).

Portanto, resuma:

Metas:

  • Eu não queria seguir a rota XmlInclude devido à dor de cabeça de manutenção.
  • Assim que uma solução fosse encontrada, eu queria que ela fosse rápida para implementar em outros aplicativos.
  • Podem ser usadas coleções de tipos abstratos, bem como propriedades abstratas individuais.
  • Eu realmente não queria me preocupar em ter que fazer coisas "especiais" nas aulas de concreto.

Problemas identificados / pontos a serem observados:

  • XmlSerializer faz uma reflexão muito legal, mas é muito limitado quando se trata de tipos abstratos (ou seja, ele só funcionará com instâncias do próprio tipo abstrato, não subclasses).
  • Os decoradores de atributo Xml definem como o XmlSerializer trata as propriedades que encontra. O tipo físico também pode ser especificado, mas isso cria um acoplamento estreito entre a classe e o serializador (não é bom).
  • Podemos implementar nosso próprio XmlSerializer criando uma classe que implementa IXmlSerializable .

A solução

Eu criei uma classe genérica, na qual você especifica o tipo genérico como o tipo abstrato com o qual trabalhará. Isso dá à classe a capacidade de "traduzir" entre o tipo abstrato e o tipo concreto, uma vez que podemos codificar o casting (ou seja, podemos obter mais informações do que o XmlSerializer pode).

Em seguida, implementei a interface IXmlSerializable , isso é bastante simples, mas ao serializar, precisamos garantir que escreveremos o tipo da classe concreta no XML, para que possamos lançá-lo de volta ao desserializar. Também é importante observar que ele deve ser totalmente qualificado, pois os conjuntos em que as duas classes estão provavelmente serão diferentes. É claro que há uma pequena verificação de tipo e outras coisas que precisam acontecer aqui.

Como o XmlSerializer não pode converter, precisamos fornecer o código para fazer isso, de forma que o operador implícito fique sobrecarregado (eu nem sabia que você poderia fazer isso!).

O código para AbstractXmlSerializer é este:

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;

namespace Utility.Xml
{
    public class AbstractXmlSerializer<AbstractType> : IXmlSerializable
    {
        // Override the Implicit Conversions Since the XmlSerializer
        // Casts to/from the required types implicitly.
        public static implicit operator AbstractType(AbstractXmlSerializer<AbstractType> o)
        {
            return o.Data;
        }

        public static implicit operator AbstractXmlSerializer<AbstractType>(AbstractType o)
        {
            return o == null ? null : new AbstractXmlSerializer<AbstractType>(o);
        }

        private AbstractType _data;
        /// <summary>
        /// [Concrete] Data to be stored/is stored as XML.
        /// </summary>
        public AbstractType Data
        {
            get { return _data; }
            set { _data = value; }
        }

        /// <summary>
        /// **DO NOT USE** This is only added to enable XML Serialization.
        /// </summary>
        /// <remarks>DO NOT USE THIS CONSTRUCTOR</remarks>
        public AbstractXmlSerializer()
        {
            // Default Ctor (Required for Xml Serialization - DO NOT USE)
        }

        /// <summary>
        /// Initialises the Serializer to work with the given data.
        /// </summary>
        /// <param name="data">Concrete Object of the AbstractType Specified.</param>
        public AbstractXmlSerializer(AbstractType data)
        {
            _data = data;
        }

        #region IXmlSerializable Members

        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null; // this is fine as schema is unknown.
        }

        public void ReadXml(System.Xml.XmlReader reader)
        {
            // Cast the Data back from the Abstract Type.
            string typeAttrib = reader.GetAttribute("type");

            // Ensure the Type was Specified
            if (typeAttrib == null)
                throw new ArgumentNullException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because no 'type' attribute was specified in the XML.");

            Type type = Type.GetType(typeAttrib);

            // Check the Type is Found.
            if (type == null)
                throw new InvalidCastException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because the type specified in the XML was not found.");

            // Check the Type is a Subclass of the AbstractType.
            if (!type.IsSubclassOf(typeof(AbstractType)))
                throw new InvalidCastException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because the Type specified in the XML differs ('" + type.Name + "').");

            // Read the Data, Deserializing based on the (now known) concrete type.
            reader.ReadStartElement();
            this.Data = (AbstractType)new
                XmlSerializer(type).Deserialize(reader);
            reader.ReadEndElement();
        }

        public void WriteXml(System.Xml.XmlWriter writer)
        {
            // Write the Type Name to the XML Element as an Attrib and Serialize
            Type type = _data.GetType();

            // BugFix: Assembly must be FQN since Types can/are external to current.
            writer.WriteAttributeString("type", type.AssemblyQualifiedName);
            new XmlSerializer(type).Serialize(writer, _data);
        }

        #endregion
    }
}

Então, a partir daí, como podemos dizer ao XmlSerializer para trabalhar com nosso serializador em vez do padrão? Devemos passar nosso tipo dentro da propriedade de tipo de atributos Xml, por exemplo:

[XmlRoot("ClassWithAbstractCollection")]
public class ClassWithAbstractCollection
{
    private List<AbstractType> _list;
    [XmlArray("ListItems")]
    [XmlArrayItem("ListItem", Type = typeof(AbstractXmlSerializer<AbstractType>))]
    public List<AbstractType> List
    {
        get { return _list; }
        set { _list = value; }
    }

    private AbstractType _prop;
    [XmlElement("MyProperty", Type=typeof(AbstractXmlSerializer<AbstractType>))]
    public AbstractType MyProperty
    {
        get { return _prop; }
        set { _prop = value; }
    }

    public ClassWithAbstractCollection()
    {
        _list = new List<AbstractType>();
    }
}

Aqui você pode ver, temos uma coleção e uma única propriedade sendo exposta, e tudo o que precisamos fazer é adicionar o parâmetro nomeado de tipo à declaração Xml, fácil! : D

NOTA: Se você usar este código, eu realmente agradeceria uma mensagem. Também ajudará a atrair mais pessoas para a comunidade :)

Agora, mas sem saber o que fazer com as respostas aqui, já que todos eles tinham seus prós e contras. Vou atualizar aqueles que considero úteis (sem ofender os que não foram) e encerrar assim que tiver a representação :)

Problema interessante e divertido de resolver! :)

Rob Cooper
fonte
Eu tive esse problema sozinho há algum tempo. Pessoalmente, acabei abandonando o XmlSerializer e usando a interface IXmlSerializable diretamente, já que todas as minhas classes precisavam implementá-la de qualquer maneira. Caso contrário, as soluções são bastante semelhantes. Bom artigo, embora :)
Thorarin
Usamos propriedades XML_ onde convertemos a lista em Arrays :)
Arcturus
2
Porque um construtor sem parâmetros é necessário para instanciar dinamicamente a classe.
Silas Hansen
1
Olá! Já faz algum tempo que procuro uma solução como essa. Eu acho que é brilhante! Embora eu não consiga descobrir como usá-lo, você se importaria de dar um exemplo? Você está serializando sua classe ou a lista, contendo seus objetos?
Daniel
1
Belo código. Observe que o construtor sem parâmetros pode ser declarado privateou protectedfazer com que ele não esteja disponível para outras classes.
tcovo,
9

Uma coisa a se observar é o fato de que no construtor XmlSerialiser você pode passar uma matriz de tipos que o serializador pode estar tendo dificuldade em resolver. Eu tive que usar isso algumas vezes, onde uma coleção ou conjunto complexo de estruturas de dados precisava ser serializado e esses tipos viviam em diferentes montagens, etc.

Construtor XmlSerialiser com parâmetro extraTypes

EDIT: Eu acrescentaria que esta abordagem tem o benefício sobre os atributos XmlInclude etc que você pode trabalhar em uma maneira de descobrir e compilar uma lista de seus possíveis tipos concretos em tempo de execução e colocá-los dentro.

Shaun Austin
fonte
Isso é o que estou tentando fazer, mas não é fácil como eu pensava: stackoverflow.com/questions/3897818/…
Luca
Este é um post muito antigo, mas para quem está procurando implementá-lo como fizemos, observe que o construtor do XmlSerializer com o parâmetro extraTypes não armazena em cache os assemblies que gera instantaneamente . Isso nos custa semanas para depurar esse vazamento de memória. Portanto, se você for usar os tipos extras com o código da resposta aceita, armazene em cache o serializador . Esse comportamento está documentado aqui: support.microsoft.com/en-us/kb/886385
Julien Lebot
3

Sério, uma estrutura extensível de POCOs nunca será serializada para XML de forma confiável. Digo isso porque posso garantir que alguém vai aparecer, estender sua aula e estragar tudo.

Você deve procurar usar XAML para serializar seus gráficos de objeto. Ele foi projetado para fazer isso, enquanto a serialização XML não.

O serializador e desserializador Xaml lida com genéricos sem problemas, coleções de classes base e interfaces também (contanto que as próprias coleções implementem IListou IDictionary). Existem algumas ressalvas, como marcar suas propriedades de coleção somente leitura com o DesignerSerializationAttribute, mas retrabalhar seu código para lidar com esses casos secundários não é tão difícil.


fonte
O link parece estar morto
bkribbs de
Ah bem. Vou detonar essa parte. Muitos outros recursos sobre o assunto.
2

Apenas uma atualização rápida sobre isso, eu não esqueci!

Apenas fazendo mais pesquisas, parece que estou no caminho certo para um vencedor, só preciso classificar o código.

Até agora, tenho o seguinte:

  • O XmlSeralizer é basicamente uma classe que faz algumas reflexões interessantes sobre as classes que está serializando. Ele determina as propriedades que são serializadas com base no Tipo .
  • A razão do problema ocorrer é porque uma incompatibilidade de tipo está ocorrendo, ele está esperando o BaseType, mas na verdade recebe o DerivedType . Embora você possa pensar que ele o trataria polimorficamente, não o faz, pois envolveria uma carga extra inteira de reflexão e verificação de tipo, o que não foi projetado para fazer.

Esse comportamento parece poder ser substituído (código pendente) criando uma classe de proxy para atuar como intermediária para o serializador. Basicamente, isso determinará o tipo da classe derivada e a serializará normalmente. Essa classe de proxy alimentará esse XML de backup da linha para o serializador principal.

Assista esse espaço! ^ _ ^

Rob Cooper
fonte
2

Certamente é uma solução para o seu problema, mas há outro problema, que de certa forma prejudica sua intenção de usar o formato XML "portátil". Coisas ruins acontecem quando você decide mudar de classe na próxima versão de seu programa e você precisa suportar ambos os formatos de serialização - o novo e o antigo (porque seus clientes ainda usam seus arquivos / bancos de dados antigos, ou eles se conectam a seu servidor usando uma versão antiga do seu produto). Mas você não pode mais usar este serializador, porque você usou

type.AssemblyQualifiedName

que parece

TopNamespace.SubNameSpace.ContainingClass+NestedClass, MyAssembly, Version=1.3.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089

que contém seus atributos de montagem e versão ...

Agora, se você tentar alterar sua versão do assembly, ou decidir assiná-la, essa desserialização não vai funcionar ...

Max Galkin
fonte
1

Eu fiz coisas semelhantes a isso. O que eu normalmente faço é ter certeza de que todos os atributos de serialização XML estão na classe concreta, e apenas fazer com que as propriedades dessa classe chamem as classes base (quando necessário) para recuperar informações que serão desserializadas quando o serializador chamar essas propriedades. É um pouco mais trabalhoso de codificação, mas funciona muito melhor do que tentar forçar o serializador a fazer a coisa certa.

O Smurf
fonte
1

Melhor ainda, usando notação:

[XmlRoot]
public class MyClass {
    public abstract class MyAbstract {} 
    public class MyInherited : MyAbstract {} 
    [XmlArray(), XmlArrayItem(typeof(MyInherited))] 
    public MyAbstract[] Items {get; set; } 
}
user2009677
fonte
2
Isso é ótimo se você conhece suas aulas, é a solução mais elegante. Se você carregar novas classes herdadas de uma fonte externa, não poderá usá-la, infelizmente.
Vladimir