C# - Serialização de Objetos - BinaryFormatter e SoapFormatter


 Na maioria das vezes que você cria um objeto usando a plataforma .NET você não se preocupa como os dados são armazenados na memória, afinal das contas a plataforma .NET faz esse trabalho para você.

No entanto, se você deseja armazenar o conteúdo de um objeto para um arquivo, enviar um objeto para outro processo, ou transmiti-lo através da rede, você tem que pensar sobre a forma como o objeto esta representado, porque você vai precisar
convertê-lo em um formato diferente. Esta conversão é chamado de serialização.

Neste artigo eu vou escrever sobre o tema serialização para a linguagem C# abordando os seguintes tópicos:

Recursos usados

O que é serialização ?

A Serialização, como implementada no namespace System.Runtime.Serialization, é o processo de serializar e desserializar objetos de modo que eles possam ser armazenados ou transferidos e depois recriados. Temos assim que:

Então se você quiser armazenar um objeto (ou vários objetos) em um arquivo para posterior recuperação, você armazena a saída da serialização, e, na próxima vez que você quiser ler os objetos, você chama os métodos da desserialização, e seu objeto é recriado exatamente como anteriormente.

Da mesma forma, se você quiser enviar um objeto para um aplicativo em execução em outro computador, você estabelece uma conexão de rede, serializa o objeto para o stream e desserializa o objeto na aplicação remota.

Como serializar um Objeto ?

Para serializar um objeto os passos são:

  1. Criar um objeto stream para tratar a saida serializada;
  2. Cria um objeto BinaryFormatter (localizado no System.Runtime.Serialization.Formatters.Binary);
  3. Chamar o método BinaryFormatter.Serialize para serializar o objeto, canaliza o resultado para o stream;

A nível de desenvolvimento, a serialização pode ser implementada com muito pouco código.

A seguir temos um exemplo de uma aplicação Console que usa os namespaces System.IO, System.Runtime.Serialization e System.Runtime.Serialization.Formatters.Binary que demonstra isso:

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace SerializacaoCSharp
{
    class Program
    {
        static void Main(string[] args)
        {
            //serialização C#
            string dados = "Este texto deve ser armazenado em um arquivo.";
          
 // cria um arquivo para salvar os dados
            FileStream fs = new FileStream("MacorattiSerializacao.Data", FileMode.Create);
         
  // Cria um objeto BinaryFormatter para realizar a serialização
            BinaryFormatter bf = new BinaryFormatter();
     
      // Usa o objeto BinaryFormatter para serializar os dados para o arquivo
            bf.Serialize(fs, dados);
       
    // Fecha o arquivo
            fs.Close();
            //Aguarda o pressionamento de uma tecla para encerrar

            Console.WriteLine("Arquivo serializado !");
            Console.ReadKey();
        }
    }
}

Ao executar o aplicativo será gerado o arquivo MacorattiSerializacao.Data na pasta bin/Debug. Ao abrir este arquivo em um bloco de notas você verá o conteúdo da string armazenada cercada por informações binárias, conforme mostra a figura a seguir:

O. NET Framework armazenou a string como texto ASCII e acrescentou alguns bytes binários antes e depois do texto para descrever os dados para o desserializador.

Se você apenas precisa armazenar uma única seqüência de texto em um arquivo, você não precisa usar a serialização pode simplesmente escrever a string diretamente para um arquivo de texto.

A serialização é útil para armazenar informações mais complexas, como a data e hora atual. A seguir temos um exemplo que demonstra, a serialização de objetos complexos. Note que ela é tão simples como serialização de uma string:

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace _2_SerializacaoComplexa
{
    class Program
    {
        static void Main(string[] args)
        {
            // Serialização C#
            // Cria um arquivo para salvar os dados
            FileStream fs = new FileStream("MacorattiSerializaoComplexa.Data", FileMode.Create);
            // Cria um objeto BinaryFormatter para realizar a serialização
            BinaryFormatter bf = new BinaryFormatter();
            // Usa o objeto BinaryFormatter para serializar os dados para o arquivo
            bf.Serialize(fs, System.DateTime.Now);
            // fecha o arquivo
            fs.Close();
            Console.WriteLine("Arquivo serializado !");
            Console.ReadKey();
        }
    }
}

Como desserializar um Objeto ?

Desserializar um objeto permite que você crie um novo objeto com base nos dados armazenados. Essencialmente, a desserialização restaura um objeto salvo.

Os passos para desserializar um objeto são:

  1. Criar um objeto stream para ler a saída serializada;
  2. Criar um objeto BinaryFormatter;
  3. Criar um novo objeto para armazenar os dados desserializados;
  4. Chamar o método BinaryFormatter.Deserialize para desserializar o objeto e convertê-lo para o tipo correto.

A nível do código, os passos para desserializar um objeto são fáceis de implementar. A seguir temos uma aplicação Console que usa os namespaces System.IO, System.Runtime.Serialization e System.Runtime.Serialization.Formatters.Binary e mostra como ler e exibir os dados strings serializados guardados no primeiro exemplor:

Obs: Para facilitar eu copei o arquivo serializado para pasta c:\dados => C:\dados\MacorattiSerializacao.Data;

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace _3_Desserializacao
{
    class Program
    {
        static void Main(string[] args)
        {
            FileStream fs = new FileStream(@"C:\dados\MacorattiSerializacao.Data", FileMode.Open);
            // Cria um objeto BinaryFormatter para realizar a dessarialização
            BinaryFormatter bf = new BinaryFormatter();
            // Cria um objeto para armazenar os dados dessarializados
            string dados = "";
            // Usa o objeto BinaryFormatter para desserializar os dados do arquivo
            dados = (string)bf.Deserialize(fs);
            // fecha o arquivo
            fs.Close();
            // exibe a string desserializada
            Console.WriteLine(dados);
            Console.ReadKey();
        }
    }
}
 
 

Desserializar um objeto complexo é a mesma coisa. Abaixo vemos o código para desserializar o exemplo anterior:

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace _4_DesserializacaoData
{
    class Program
    {
        static void Main(string[] args)
        {
            // Abreo arquivo para ler os dados
            FileStream fs = new FileStream(@"c:\dados\MacorattiSerializaoComplexa.Data", FileMode.Open);
            // Cria um objeto BinaryFormatter para realizar a dessarialização
            BinaryFormatter bf = new BinaryFormatter();
            // Cria um objeto para armazenar os dados dessarializados
            DateTime previousTime = new DateTime();
            // Usa o objeto BinaryFormatter para desserializar os dados do arquivo
            previousTime = (DateTime) bf.Deserialize(fs);
            // fecha o arquivo
            fs.Close();
            // Exibe o tempo desserializado
            Console.WriteLine("Dia: " + previousTime.DayOfWeek + ",Hora: " + previousTime.TimeOfDay.ToString());
            Console.ReadKey();
        }
    }
}

Como criar classes que possam ser serializadas

Você pode serializar e desserializar classes adicionando o atributo Serializable na classe, dessa forma você ou outros desenvolvedores que usarem a classe poderão armazenar ou transferir instâncias da classe. Mesmo que você no momento não necessite serializar é uma boa prática habilitar suas classes para usar o recurso no futuro. (Aqui cabe o bom senso. Se houver qualquer suspeita de que as classes poderão necessitar de serialização no futuro é bom deixar o terreno preparado.)

Quando a classe for serializada todos os membros, incluídos membros privados serão serializados.

Nota: A serialização pode permitir que outro código modifique ou acesse os dados da instância da classe que de outra forma seriam inacessíveis. Por isso, o código que realiza a serialização necessita do atributo SecurityPermission, do namespace System.SecurityPermission com a flag SerializationFormatter definida. Esta permissão não é dada para código em intranet ou download de internet, somente o código no computador local é concedida a permissão.

Como desabilitar a serialização de membros específicos

Alguns membros de sua classe, como valores temporários ou calculados, pode não precisar serem serializados. Vejamos um exemplo hipotético de uma classe CarrinhoItens usada em um comércio eletrônico:

    [Serializable]
    class CarrinhoItens
    {
        public int produtoId;
        public decimal preco;
        public int quantidade;
        public decimal total;

        public CarrinhoItens(int _produtoID, decimal _preco, int _quantidade)
        {
            produtoId = _produtoID;
            preco = _preco;
            quantidade = _quantidade;
            total = preco * quantidade;
        }
    }
[Serializable]
class CarrinhoItens
{
   public int produtoId;
   public decimal preco;
   public int quantidade;
   [NonSerialized]
   public decimal total;


  public CarrinhoItens(int _produtoID, decimal _preco, int _quantidade)
  {
    produtoId = _produtoID;
    preco = _preco;
    quantidade = _quantidade;
    total = preco * quantidade;
}
}

A classe CarrinhoItens inclui três membros que devem ser fornecidos pelo aplicativo quando o objeto for criado. O quarto membro, total, é dinamicamente calculado multiplicando-se o preço e quantidade. Se esta classe for serializada como esta, o total seria armazenado com o objeto serializado, desperdiçando uma pequena quantidade de armazenamento.

Para reduzir o tamanho do objeto serializado (e assim reduzir os requisitos de armazenamento quando se escreve o objeto serializado para o disco, e os requisitos de largura de banda durante a transmissão do objeto serializado em toda a rede), adicione o atributo NonSerialized para o membro no total, conforme mostrado no código acima:

Agora, quando o objeto for serializado, o membro total será omitido. Da mesma forma, o membro total não será inicializado quando o objeto for serializado. No entanto, o valor total ainda deve ser calculado antes do objeto serializado ser usado.

Para ativar sua classe para inicializar automaticamente um membro não serializado, utilize a Interface IDeserializationCallback, a seguir, implemente IDeserializationCallback.OnDeserialization. Cada vez que sua classe for serializado, o runtime irá chamar a método IDeserializationCallback.OnDeserialization após a desserialização estar completa.

O exemplo a seguir mostra a classe CarrinhoItens modificada para não serializar o valor total e, para calcular automaticamente o valor na desserialização:

using System;
using System.Runtime.Serialization;
    [Serializable]
    class CarrinhoItens : IDeserializationCallback
    {
        public int produtoId;
        public decimal preco;
        public int quantidade;
        public decimal total;

        public CarrinhoItens(int _produtoID, decimal _preco, int _quantidade)
        {
            produtoId = _produtoID;
            preco = _preco;
            quantidade = _quantidade;
            total = preco * quantidade;
        }
       void IDeserializationCallback.OnDeserialization(Object sender)
        {
            // depois da serialização , calcula o total
            total = preco * quantidade;
        }
    }

Com a implementação de OnDeserialization o membor total agora é adequadamente definido e disponível para a aplicação depois da classe ser desserializada.

Como fornecer a compatibilidade de versão

Você pode ter problemas de compatibilidade de versão, se você tentar desserializar um objeto que foi serializado por uma versão anterior da sua aplicação.

Especificamente, se adicionar um membro na classe e tentar desserializar um objeto que não tem esse membro, o runtime irá lançar uma exceção. Em outras palavras, se você adicionar um membro a uma classe na versão x.1 do seu aplicativo, ele não será capaz de desserializar um objeto criado pela versão x.0.

Para superar essa limitação, você tem duas opções:

  1. Implementar uma serialização personalizada que é capaz de importar objetos serializados de versões anteriores;
  2. Aplicar o atributo OptionalField aos membros recém-adicionados que podem causar problemas de compatibilidade de versão;

O atributo OptionalField não afeta o processo de serialização. Durante a desserialização, se o membro não foi serializado, o runtime vai deixar o valor do membro como nulo ao invés de lançar uma exceção. O exemplo seguinte mostra como usar o atributo OptionalField:

[Serializable]
Public class CarrinhoItens
{
  public int produtoId;
  public decimal preco;
  public int quantidade;
  [NonSerialized]
  public decimal total;


  [OptionalField]
  public bool ativo;

public CarrinhoItens(int _produtoID, decimal _preco, int _quantidade)
{
    produtoId = _produtoID;
    preco = _preco;
    quantidade = _quantidade;
    total = preco * quantidade;
}
}

Se você precisar iniciar membros opcionais, deverá ou implementar a interface IDeserializationCallback como já comentei ou responder a eventos de serialização que irei tratar mais adiante.

As melhores práticas para a compatibilidade de versão

Para assegurar um comportamento apropriado no versionamento procure seguir as regras abaixo quando alterar uma classe:

Escolhendo o formato da serialização

A plataforma .NET inclui dois métodos para formatar os dados serializados no Namespace System.Runtime.Serialization, ambos implementam a interface IRemotingFormatter:

  1. BinaryFormatter - Localizado no namespace System.Runtime.Serialization.Formatters.Binary, este formatador é a maneira mais eficiente para serializar objetos que serão lidos somente aplicações da plataforma .NET.
  2. SoapFormatter - Localizado no namespace System.Runtime.Serialization.Formatters.Soap, este formatador baseado em XML é a maneira mais confiável para serializar objetos que serão transmitidos através de uma rede ou lido outros aplicativos.

O formatador SoapFormatter é mais adequado para atravessar com êxito firewalls do que BinaryFormatter.

Resumindo, você deve escolher BinaryFormatter apenas quando você sabe que todos os clientes que irão abrir os dados serializados serão aplicações da plataforma .NET. Portanto, se você for escrever objetos para o disco para serem lidos mais tarde pela sua aplicação, o formato BinaryFormatter é perfeito.

Use SoapFormatter quando outros aplicativos podem ler seus dados serializados e enviar dados através de uma rede. SoapFormatter também funciona de forma confiável em situações onde você pode escolher BinaryFormatter, mas o objeto serializado pode consumir de três a quatro vezes mais espaço.

Como SoapFormatter é baseado em XML, ele destina-se principalmente a ser usado por serviços Web SOAP. Se o seu objetivo é armazenar objetos em um documento aberto, baseado em padrões que pode ser consumido por aplicações rodando em outras plataformas, a maneira mais flexíveis de realizar a serialização é escolher a serialização XML.

Como usar SoapFormatter

Para usar SoapFormatter, adicione uma referência ao assembly System.Runtime.Serialization.Formatters.Soap.dll ao seu projeto. (Ao contrário de BinaryFormatter, ela não está incluído por padrão). Então escreva o código exatamente como você gostaria de usar BinaryFormatter, mas substitua a classe BinaryFormatter pela a classe SoapFormatter.

Escrever código para BinaryFormatter e SoapFormatter é muito parecido mas os dados serializados são muito diferentes. O exemplo a seguir mostra um objeto com 3 membros serializado com SoapFormatter:

<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <SOAP-ENV:Body>
     <a1:CarrinhoItens id="ref-1">
       <produtoId>100</produtoId>
       <preco>10.25</preco>
       <quantidade>2</quantidade>
     </a1:CarrinhoItens>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Exemplo de serialização com SoapFormatter:

No projeto lembre-se de incluir a referência a System.Runtime.Serialization.Formatters.Soap ao projeto;

Selecione o projeto e clique em PROJECT -> Add reference e selecione o assembly conforme abaixo:

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Soap;

[Serializable]
public class Carro
{
    public string Fabricante;
    public string Modelo;
    public uint Ano;
    public byte Cor;
}

public static class Program
{
    public static int Main(string[] args)
    {
        Carro veiculo = new Carro();

  
     veiculo.Fabricante = "Ford";
        veiculo.Modelo = "Fiesta Sedan";
        veiculo.Ano = 2009;
        veiculo.Cor = 3;


        FileStream streamCarro = new FileStream("CarroStream.car", FileMode.Create);
        SoapFormatter soapCarro = new SoapFormatter();

        soapCarro.Serialize(streamCarro, veiculo);
        return 0;
    }
}

Note que a única mudança foi usar a classe SoapFormatter ao inves da classe BinaryFormatter.

O arquivo CarroStream.car serializado usando SoapFormatter é vista na pasta bin/debug:

<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV=
"http://schemas.xmlsoap.org/soap/envelope/"

xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<a1:Carro id="ref-1" xmlns:a1="http://schemas.microsoft.com/clr/assem
/SoapFormatter%2C%20Version%3D1.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<Fabricante id="ref-3">Ford</Fabricante>
<Modelo id="ref-4">Fiesta Sedan</Modelo>
<Ano>2009</Ano>
<Cor>3</Cor>
</a1:Carro>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Como controlar a serialização SOAP

Podemos controlar a serialização SOAP usando os atributos descritos a seguir:

Atributo Aplica-se a Especifica
SoapAttribute Campo publico,propriedade, parâmetro ou valor retorno O membro da classe será serializado como um atributo XML
SoapElement Campo publico,propriedade, parâmetro ou valor retorno A classe será serializada como um elemento XML
SoapEnum Campo publico que é um identificador enumeration O nome do elemento de um membro enumeration
SoapIgnore Propriedades e campos públicos A propriedade ou campo será ignorado quando a classe for serializada
SoapInclude Declarações de classes public derivadas e métodos públicos
para documentos WSDL - Web Services Description Language
O tipo será incluído quando da geração de schemas.

Os atributos de serialização SOAP funcionam de forma idêntica para os atributos da serialização XML.

Para concluir 3 regras básicas para serialização:

  1. Se estiver na dúvida, marque uma classe como Serializable. Mesmo que você não precisar serializar agora, você pode precisar da serialização mais tarde. Ou outro desenvolvedor pode precisar serializar uma classe derivada.
  2. Marque os membros calculados ou temporários como NonSerialized.
  3. Use SoapFormatter quando você precisar de portabilidade. Use BinaryFormatter para maior eficiência.

Pegue o projeto completo aqui: SerializacaoCSharp.zip

Mat 6:5 E, quando orardes, não sejais como os hipócritas; pois gostam de orar em pé nas sinagogas, e às esquinas das ruas, para serem vistos pelos homens. Em verdade vos digo que já receberam a sua recompensa.

Mat 6:6 Mas tu, quando orares, entra no teu quarto e, fechando a porta, ora a teu Pai que está em secreto; e teu Pai, que vê em secreto, te recompensará.

Referências:


José Carlos Macoratti