Revista MSDN Magazine Edição 10 - Serializando objetos com .Net

Artigo Originalmente Publicado na MSDN Magazine Edição 10

Clique aqui para ler todos os artigos desta edição

 

Serializando objetos com .Net

por Wallace Cézar Sales dos Santos

Um dos grandes desafios do desenvolvimento orientado a objetos é a serialização e desserialização dos objetos. O .Net Framework oferece recursos importantes e facilitadores para a realização dessa tarefa durante o ciclo de vida de um objeto.  A serialização é utilizada porque precisamos, basicamente, persistir o estado dos objetos.

Introdução

Serialização é o processo pelo qual convertemos o estado de um objeto num formato em que possamos persisti-lo ou transportá-lo. Desserialização é o processo inverso, permitindo assim, uma facilidade maior em manipular objetos durante espaços de tempo e limites de aplicação, conforme a necessidade identificada e projetada.

Utilizamos serialização de diversas formas nos sistemas: para armazenar um objeto no banco de dados ou em disco, serializar para compartilhar objetos entre várias aplicações ou ainda, transportar objetos através de uma rede, como nas operações de Remoting. O .Net Framework nos fornece nativamente duas técnicas para serialização de objetos: Serialização binária e Serialização por XML e SOAP.

Por que Serializar?

A primeira coisa que precisamos ter em mente é “Por que queremos serializar um objeto?”. As razões podem ser muitas, porém estarão sempre relacionadas com duas operações: A primeira é armazenar o estado de um objeto em uma cópia exata que será recuperada posteriormente; a segunda razão é transportar o estado do objeto entre domínios de aplicações, que podem ser num mesmo computador, numa intranet ou mesmo na Internet, através de Web Services, por exemplo.

No primeiro caso pensamos imediatamente às operações relacionadas com a sigla CRUD e os bancos de dados: Create, Read, Update e Delete. Afinal, o normal é fazermos exatamente isto: desenhamos métodos dos objetos utilizando bancos de dados para armazenar seu estado. Só que esta atividade pode ser muito difícil de realizar a medida em que a complexidade do objeto aumenta e passamos a não ter mais apenas um objeto e sim vários objetos complexos. A tarefa, até então trivial, ganha exponencial complexidade. Notem, não quero de forma alguma passar a idéia de que isto é errado, porém é importante sabermos que temos opções a este árduo trabalho (determinada através dos requerimentos e da correta arquitetura do software), uma vez que a serialização nos fornece um mecanismo bastante interessante e conveniente para alcançarmos este objetivo, com um esforço substancialmente menor.

Para a segunda opção, é importante termos em mente que um objeto somente é válido no domínio da aplicação que o criou. Se este objeto não for serializável, qualquer tentativa de transferi-lo de um domínio para outro resultará em fracasso. No .Net realizamos as operações de transferência de objetos entre domínios de aplicação através do uso de Remoting, que não é o escopo neste artigo.

Serializando um objeto

A serialização no .Net é um processo relativamente simples. Precisamos apenas marcar a classe desejada como Serializável, que pode ser feito através da adição do atributo à classe desejada, como vemos na Listagem 1. Este atributo, quando adicionado à uma classe, indica que suas instâncias poderão ser serializadas através de operações de Reflection. Para a classe em si, o trabalho está finalizado.

Listagem 1: Marcando uma classe como serializável

Public Class Prancha

   'Adicione aqui o código da Classe

End Class

 

A grande questão vem com a necessidade do uso da serialização. Uma vez identificados os requisitos no sistema onde faremos uso desta tecnologia, cabe-nos realizar as tarefas necessárias para implementá-la no código. A tarefa continua simples, mas agora precisamos tomar alguns cuidados e realizar algumas pequenas tarefas para o completo sucesso da operação. A primeira é referenciar em nossa classe os namespaces que  permitirão o uso de serialização: System.Runtime.Serialization, que contém as classes utilizadas para as operações de serialização e desserialização de objetos. A segunda é definir qual formatador utilizaremos e fazermos a também a declaração do namespace para uso na classe, que pode ser o System.Runtime.Serialization.Formatters.[Binary]ou[Soap]. O formatador define como será armazenado o estado do objeto: o primeiro caso para serialização binária e o segundo em formato XML/SOAP. Cabe ressaltar que se você desejar utilizar a opção com SOAP, deverá fazer uma referência  separada ao Assembly de nome igual antes no projeto (esta classe encontra-se num assembly diferente).

O passo seguinte é criar uma instância do formatador escolhido (uma das opções citadas anteriormente) e uma instância de uma classe derivada da classe System.IO.Stream (FileStream, por exemplo). Esta classe Stream serve para que possamos, neste caso, persistir em disco o estado do objeto. Depois de criadas ambas as intâncias, executamos o método Serialize() do formatador, como podemos verificar na Listagem 2.

Listagem 2: Implementando a serialização

'Criando uma instância do Formatador

Dim pranchaFormatter As IFormatter = New SoapFormatter

'Criando uma instância do Stream

Dim stream As Stream = New FileStream("Prancha.xml", FileMode.Create, FileAccess.Write, FileShare.None)

'Serializando o objeto

pranchaFormatter.Serialize(stream, prancha)

'Fechando o Stream

stream.Close()

 

Note na Listagem 2 que em nenhum momento definimos o que será ou não serializado. É importante ter em mente que o mecanismo de serialização automaticamente armazena o estado de todas as variáveis de classe (campos), desde que definidos como privados. A Listagem 3 demonstra a implementação completa da Classe Prancha e dela somente será serializado as informações armazenadas nos campos da região “Campos internos”. Porém, chamo a atenção para um detalhe: o campo _tipoPrancha está marcado com o atributo , e isso significa que esta informação não será serializada pelo mecanismo. Isto é necessário pois haverá ocasiões onde não desejaremos que o estado de uma determina propriedade seja armazenado, como por exemplo, a senha de um usuário do sistema.

Listagem 3: a classe Prancha

Public Class Prancha

'Adicione aqui o código da Classe

#Region " Campos internos"

Private _numeroQuilha As NumeroQuilha = NumeroQuilha.TriQuilha

Private _tamanho As Single = 6.0F

Private _tipoRabeta As TipoRabeta = TipoRabeta.Squash

    Private _largura As Single = 19.0F

Private _proprietario As String = String.Empty

Private _dataFabricacao As DateTime = DateTime.Now

Private _tipoPrancha As TipoPrancha = TipoPrancha.HotDog

Private _shaper As Shaper = Nothing

#End Region

 

#Region " Constantes"

Private Const PRANCHA_PEQUENO As String = "O tamanho informado é muito pequeno."

Private Const PRANCHA_ESTREITA As String = "A largura informada é muito estreita."

Private Const PROPRIETARIO_VAZIO As String = "É necessário informar o nome do proprietário da prancha."

Private Const DATA_INVALIDA As String = "Data informada inválida."

#End Region

 

#Region " Construtores"

Public Sub New()

MyBase.New()

End Sub

 

Public Sub New(ByVal proprietario As String, ByVal tamanho As String)

Me.New()

With Me

.Proprietario = proprietario

.Tamanho = tamanho

End With

End Sub

#End Region

 

#Region " Propriedades"

Public Property NumeroQuilha() As NumeroQuilha

Get

Return Me._numeroQuilha

End Get

Set(ByVal Value As NumeroQuilha)

Me._numeroQuilha = Value

End Set

End Property

 

Public Property Tamanho() As Single

Get

Return Me._tamanho

End Get

Set(ByVal Value As Single)

If (Value < 4.5F) Then Throw New ApplicationException(Me.PRANCHA_PEQUENO)

Me._tamanho = Value

End Set

End Property

 

Public Property TipoRabeta() As TipoRabeta

Get

Return Me._tipoRabeta

End Get

Set(ByVal Value As TipoRabeta)

Me._tipoRabeta = Value

End Set

End Property

 

Public Property Largura() As Single

Get

Return Me._largura

End Get

Set(ByVal Value As Single)

If (Value < 18.0F) Then Throw New ApplicationException(Me.PRANCHA_ESTREITA)

Me._largura = Value

End Set

End Property

 

Public Property Proprietario() As String

Get

Return Me._proprietario

End Get

Set(ByVal Value As String)

If ((Value Is String.Empty) OrElse (Value Is Nothing)) Then Throw New ApplicationException(Me.PROPRIETARIO_VAZIO)

Me._proprietario = Value

End Set

End Property

 

Public Property DataFabricacao() As DateTime

Get

Return Me._dataFabricacao

End Get

Set(ByVal Value As DateTime)

If ((Value < DateTime.Today) OrElse (Value = Nothing)) Then Throw New ApplicationException(Me.DATA_INVALIDA)

Me._dataFabricacao = Value

End Set

End Property

 

Public Property TipoPrancha() As TipoPrancha

Get

Return Me._tipoPrancha

End Get

Set(ByVal Value As TipoPrancha)

Me._tipoPrancha = Value

End Set

End Property

 

Public ReadOnly Property Shaper() As Shaper

Get

If (Me._shaper Is Nothing) Then Me._shaper = New Shaper

Return Me._shaper

  End Get

End Property

 

#End Region

 

End Class

 

Uma vez executado o código apresentado na Listagem 2 sobre a classe apresentada na Listagem 3, teremos como resultado o arquivo Prancha.xml, como vemos na Listagem 4. Um detalhe que é importante observar é o fato da serialização ter ocorrido inclusive para os objetos membros – campo _shaper, que é um tipo Shaper, sem que tenhamos qualquer linha de codificação a mais para realizar esta tarefa.

Listagem 4: Arquivo gerado pelo formatador SoapFormatter

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:encoding>

xmlns:a1="http://schemas.microsoft.com/clr/nsassem/ExemploSerializacao/ExemploSerializacao%2C%20Version%3D1.0.1598.39275%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">

<_numeroQuilha>TriQuilha

<_tamanho>7.4

<_tipoRabeta>Squash

<_largura>19.3

<_proprietario id="ref-3">Wallace Cézar Sales dos Santos

<_dataFabricacao>2004-06-01T00:00:00.0000000-03:00

<_shaper href="#ref-4"/>

<_nome id="ref-5">Paulo Araújo

<_quantidadePranchas>10000

<_tempoShaper>25

 

Poderíamos ter utilizado o formatador BinaryFormatter para a mesma operação acima. A tarefa demostrada na Listagem 2 seria implementada praticamente da mesma forma (mudaríamos apenas o tipo Formatter instanciado – de SoapFormatter para BinaryFormatter – e o nome do arquivo a ser gravado, para Prancha.bin, por exemplo). O BinaryFormatter é extremamente eficiente e produz um conjunto de bytes compactos, sendo uma solução bastante atraente e, por que não dizer, ideal quando temos um ambiente completamente .NET. Agora, se o requisito for portabilidade utilize o SoapFormatter.

Desserializando um objeto

Se serialização no .Net demonstrou ser um processo relativamente simples, o processo de desserialização não deixa nada a desejar. Veja na Listagem 5 que praticamente realizamos a difícil tarefa de “copy & paste”! Somente se prestarmos atenção nos detalhes é que notaremos uma mudança nos argumentos passados no construtor do objeto FileStream e que executamos o método Deserialize() do objeto SoapFormatter.

Listagem 5: O processo de desserialização de objetos

'Criando uma instância do Formatador

Dim pranchaFormatter As IFormatter = New SoapFormatter

'Criando uma instância do Stream

Dim stream As Stream = New FileStream("Prancha.xml", FileMode.Open, FileAccess.Read, FileShare.Read)

'Desserializando o objeto

Dim prancha As Prancha = CType(pranchaFormatter.Deserialize(stream), Prancha)

'Fechando o Stream

stream.Close()

 

Observe que no processo de desserialização não criamos primeiro uma instância do objeto Prancha, para então lhe atribuir os valores. Isso acontece por questões de performance. Não quer dizer que se você quiser primeiro criar uma instância do objeto e depois atribuir ocorrerá um erro, mas implica na performance.

E a segurança?

Indo direto ao cerne da questão, observe a seguinte regra: somente serialize aquilo que realmente precisa ser serializado. Esta regra é importante porque quando serializamos um objeto, criamos uma oportunidade de outro código ver ou mesmo modificar uma instância de um objeto (afinal, não é isso que está serializado?).

Um segundo cuidado a ser observado é quando serializar um objeto, realize a operação de serialização somente nos campos que você realmente precisa serializar. Podemos controlar isso através da adição do atributo , visto na Listagem 3. Procure adicionar este atributo sempre que o campo armazenar informações sensíveis.

Utilize também as definições de permissões existentes no .Net Framework. Para execução de operações de serialização, uma permissão especial é necessária: SecurityPermission, que descreve um conjunto de permissões aplicadas à um determinado código, com o flag (propriedade Flags) SerializationFormatter – valor determinado pela enumeração SercurityPermissionFlag –  especificado. Sob as políticas padrão determinadas pelo .NET Framework, esta permissão não impede códigos baixados da Internet ou códigos de intranet de executar serialização na máquina local. Somente será permitida a execução de códigos locais do computador.

Por fim, outra forma de implementarmos segurança nas operações de serialização é realizando a implementação da interface ISerializable, definindo assim uma forma customizada de serialização do objeto desejado, como vemos na Listagem 6, e então, explicitamente adicionar o atributo SecurityPermissionAttribute com a permissão SerializationFormatter especificado ao método e ainda, garantindo no método que nenhuma informação sensível será retornada pelo método.

Customizando a serialização

Quando implementamos a interface ISerializable, passamos a ter a capacidade de definir como o nosso objeto será serializado. O fato de implementarmos esta interface não nos desobriga do uso do atributo para que seja possível a realização desta operação em nosso objeto, mas permite-nos definir como ocorrerá a sua serialização. Isso é interessante em situações como àquelas apresentadas no tópico “E a Segurança?” ou em casos onde um determinado valor de um campo serializado torna-se inválido após a desserilização mas é necessário fornecer um valor para ele durante a reconstrução do objeto.

A Listagem 6 apresenta a classe Surfista, que implementa a interface ISerializable. Observe no código que criamos um construtor especial, que é utilizado quando o objeto é desserializado, e implementamos o procedimento GetObjectData definido na interface, método este que popula um objeto SerializationInfo com todos os dados necessários para manter o estado de nosso objeto, ou seja, durante a chamada da serialização de nosso objeto é nossa a responsabilidade de determinar quais dados serão mantidos o estado e como será.

O preenchimento do objeto SerializationInfo é muito simples, tratando-se apenas da execução do método AddValue(),  onde passamos uma string qualquer como chave e o valor a ser armazenado.

Listagem 6: Implementando a interface Iserializable

Public Class Surfista

Implements ISerializable

 

#Region " Campos internos"

Private _nome As String = String.Empty

Private _idade As Integer = 0

Private _posicao As PosicaoPrancha = PosicaoPrancha.Regular

Private _localDe As String = String.Empty

Private _isProfessional As Boolean = False

#End Region

 

#Region " Constantes"

Private Const NOME_VAZIO As String = "É necessário informar o nome do surfista."

Private Const IDADE_INVALIDA As String = "A idade informada é inválida."

#End Region

 

#Region " Construtores"

Public Sub New()

MyBase.New()

End Sub

Public Sub New(ByVal nome As String)

Me.New()

Me.Nome = nome

End Sub

Protected Sub New(ByVal info As SerializationInfo, ByVal context As StreamingContext)

Me.New()

With Me

._nome = info.GetString("nome")

._idade = info.GetInt32("idade")

._posicao = info.GetValue("posicao", PosicaoPrancha.GetType())

._localDe = info.GetString("localDe")

._isProfessional = info.GetBoolean("isProfessional")

End With

End Sub

#End Region

 

#Region " Propriedades"

Public Property Nome() As String

Get

Return Me._nome

End Get

Set(ByVal Value As String)

If ((Value Is Nothing) OrElse (Value Is String.Empty)) Then Throw New ApplicationException(Me.NOME_VAZIO)

Me._nome = Value

End Set

End Property

 

Public Property Idade() As Integer

Get

Return Me._idade

End Get

Set(ByVal Value As Integer)

If (Value < 0) Then Throw New ApplicationException(Me.IDADE_INVALIDA)

Me._idade = Value

End Set

End Property

 

Public Property PosicaoPrancha() As PosicaoPrancha

Get

Return Me._posicao

End Get

Set(ByVal Value As PosicaoPrancha)

Me._posicao = Value

End Set

End Property

 

Public Property LocalDe() As String

Get

Return Me._localDe

End Get

Set(ByVal Value As String)

Me._localDe = Value

End Set

End Property

 

Public Property IsProfessional() As Boolean

Get

Return Me._isProfessional

End Get

Set(ByVal Value As Boolean)

Me._isProfessional = Value

End Set

End Property

 

#End Region

 

#Region " Métodos"

 

_

Public Sub GetObjectData(ByVal info As System.Runtime.Serialization.SerializationInfo, _

ByVal context As System.Runtime.Serialization.StreamingContext) _

Implements System.Runtime.Serialization.ISerializable.GetObjectData

With info

.AddValue("nome", Me._nome)

.AddValue("idade", Me._idade)

.AddValue("posicao", Me._posicao)

.AddValue("localDe", Me._localDe)

.AddValue("isProfessional", Me._isProfessional)

End With

End Sub

#End Region

 

End Class

 

A recuperação dos valores ocorre com igual facilidade. No construtor especial adicionado, recebemos o objeto SerializationInfo e através de seus métodos GetValue() (sendo que a palavra Value poderá ser substituído por String, Int32, Boolean, etc) e, em seguida, informamos como argumento uma String referenciando a chave vinculada ao campo interno, criada no momento da serialização.

Um último ponto a ser observado e não menos importante envolve a implementação de herança. Quando desenvolvemos uma classe que herda de uma classe que implementa a interface ISerializable, devemos igualmente implementar o construtor e o método GetObjectData(), porém agora sobrepondo (Shadows) o método da classe pai. Sempre lembre que os objetos são desserializados de dentro para fora, e por isso devemos tomar alguns cuidados para evitar resultados indesejados. Um deles seria, na classe filha, chamar sempre no construtor de desserialização primeiramente o construtor da classe pai, afim de que o objeto seja completamente reconstruído. Outra questão importante é a chamada de métodos durante o processo de desserialização, pois o objeto pode ainda não possuir seu estado completamente reestabelecido e, assim, não responder corretamente à necessidade. A solução para isso é implementar a interface IDeserializationCallback, onde o método OnDeserialization é automaticamente chamado quando o objeto é inteiramente desserializado.

Uma outra opção

Sim, existe uma outra forma de serializar objetos na tecnologia .NET, nativa do ambiente e igualmente fácil de implementar, encontrada no namespace System.Xml.Serialization, mais precisamente na classe XmlSerializer, que serializa e desserializa objetos dentro de documentos XML, tornando desnecessário o uso do atributo . A grande diferença está no que é serializado. Se no atributo são serializados os campos definidos como privados, aqui serializamos os campos / propriedades definidos como públicos. Isso fica claro se compararmos o resultado da serialização da classe Prancha (Listagem 3), apresentada na Listagem 4, com o resultado da serialização utilizando a classe XmlSerializer, apresentado na Listagem 7. Note que na primeira serialização, o campo _tipoPrancha não é serializado, justamente porque está marcado para não ser. No segundo caso, podemos ver claramente o valor serializado, através do elemento no documento. Então se temos um grande facilitador em termos de padrões e interoperabilidade, passamos a ver nossas preocupações com segurança aumentadas.

Listagem 7: Resultado da serialização utilizando a classe XmlSerializer

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

  TriQuilha

  7.4

  Squash

  19.3

  Wallace Cézar Sales dos Santos

  2004-06-04T00:00:00.0000000-03:00

  Fun

 

Os processos de serialização/desserialização são muito parecidos com os anteriormente vistos. Primeiro definimos uma instância da classe XmlSerializer, passando como argumento no construtor o tipo que planejamos manipular. Em seguida, criamos o arquivo onde inserimos os dados serializados (um tipo FileStream, por exemplo) e então executamos o método Serialize(), informando o stream criado e a instância do objeto desejado como argumentos, demonstrado na Listagem 8. Para o processo de desserialização, invocamos o método Deserialize(), com a mesma sequência já vista nas operações com o atributo .

Listagem 8: Serializando e desserializando com a classe XmlSerializer

'Serializando o objeto

Dim pranchaXml As New XmlSerializer(GetType(Prancha))

Dim writer As New FileStream("prancha1.xml", FileMode.Create)

pranchaXml.Serialize(writer, prancha)

writer.Close()

 

'Desserializando o objeto

Dim pranchaXml As New XmlSerializer(GetType(Prancha))

Dim file As New FileStream("prancha1.xml", FileMode.Open)

Dim prancha As Prancha = CType(pranchaXml.Deserialize(file), Prancha)

file.Close()

 

A Listagem 8 apresenta a forma mais simples de implementar serialização com XmlSerializer. Existem muitos outros recursos que se por um lado perdemos em questões de segurança, ganhamos no que se refere à flexibilidade na serialização do objeto, pois podemos controlar a ação de forma a garantir que o XML resultante seja conforme um schema determinado. Podemos definir se o campo ou propriedade serializada será através de um elemento ou atributo, podemos especificar qual namespace do XML usaremos e podemos mudar os nomes dos campos ou propriedades para um que julguemos mais próprios.

Por fim, se estiver em dúvidas sobre qual técnica utilizar, considere tópicos como a necessidade de serializar além do estado do objeto, valores sobre identidade do tipo e informações do assembly, necessidade de publicar dados privados, a classe não possuir um construtor padrão, onde se forem mandatórios, não utilize o XmlSerializer.

Conclusão

Neste artigo pudemos aprender o que é serialização e ter uma visão ampla sobre os recursos que o .NET Framework nos oferece para que possamos executar essas operações, customizar e ainda avaliar as questões de segurança envolvidas. Serialização é um recurso interessante e poderoso que podemos e devemos aplicar em nossas aplicações, desde que avaliadas todas as questões envolvidas. Lembre-se: sempre que houver a necessidade de armazenar ou transportar o estado de um objeto, serialização pode ser a melhor opção.

 

Download disponível em http://www.neoficio.com.br/msdn

 

OLHO: A primeira coisa que precisamos ter em mente é “Por que queremos serializar um objeto?”

 

Um segundo cuidado a ser observado é quando serializar um objeto, realize a operação de serialização somente nos campos que você realmente precisa serializar

Ebook exclusivo
Dê um upgrade no início da sua jornada. Crie sua conta grátis e baixe o e-book

Artigos relacionados