Desenvolvimento - C#

Por dentro da classe Message

A finalidade deste artigo é demonstrar a classe Message (System.ServiceModel.Channels), responsável por representar o envelope SOAP dentro da infraestrutura do WCF.

por Israel Aéce



Em qualquer tecnologia de aplicação distribuída, há vários elementos que trabalham em conjunto para fazer tudo funcionar. Na maioria das vezes que utilizamos alguma dessas tecnologias, não nos preocupamos como o processo acontece nos bastidores. Na verdade, a idéia é essa mesma, ou seja, no primeiro momento, não há necessidade de conhecer níveis mais baixos, justamente porque a tecnologia os abstrai.

Já quando as coisas começam a ficar mais complexas, talvez seja o momento de entender o funcionamento interno de forma mais aprofundada, a fim de analisar o porque daquele determinado comportamento, ou ainda, caso você queira customizar ou interceptar algum ponto da execução. A finalidade deste artigo é demonstrar a classe Message (System.ServiceModel.Channels), responsável por representar o envelope SOAP dentro da infraestrutura do WCF.

Em vários artigos anteriores, eu mencionei a classe Message, principalmente quando falado sobre a codificação e extensibilidade da mensagem. Do lado do remetente da mensagem, haverá um encoder, responsável por recuperar a instância da classe Message, transformá-la em um stream de bytes e enviá-la para o destino através da rede. Já do lado do destinatário, o encoder captura a sequência de bytes da rede e a transforma em uma instância da classe Message novamente, para que a mesma possa ser processada.

A instância da classe Message, que no primeiro momento é criada pelo runtime, é a forma tipada de acesso ao envelope SOAP, que traz diversas informações importantes. Entre elas temos a operação requisitada pelo cliente, seus respectivos parâmetros e dados contextuais, refletindo o nível de segurança, transações, mensagens confiáveis, etc. O protocolo SOAP é baseado em XML, e sua estrutura consiste basicamente em duas seções: headers e body. Os headers são as informações contextuais da requisição (não confunda headers do SOAP com headers de um protocolo como o HTTP); já o body é a seção dentro do SOAP que armazena as informações (parâmetros) exigidos pela operação que será executada. Abaixo temos a representação XML da classe Message:

<s:Envelope
xmlns:a="http://www.w3.org/2005/08/addressing"
xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Header>
<a:Action s:mustUnderstand="1">http://www.israelaece.com/srv/cadastrar</a:Action>
</s:Header>
<s:Body>
<Program.Usuario
xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication1">
<Nome>Israel Aece</Nome>
</Program.Usuario>
</s:Body>
</s:Envelope>

É importante dizer que nem sempre o conteúdo da classe Message vai ser representado por XML baseando-se no SOAP. Em alguns cenários, a serialização da mensagem deverá seguir um outro formato, como por exemplo, quando você expõe um serviço através de POX (Plain Old XML), eliminando a estrutura SOAP. Neste caso, a classe Message conseguirá se adaptar perfeitamente a esse formato.

Antes de analisarmos a estrutura da classe Message, devemos entender sobre alguns novos tipos que foram adicionados junto com o WCF para melhorar a serialização/deserialização da classe Message. A Microsoft adicionou três novos tipos debaixo do namespace System.Xml (Assembly System.Runtime.Serialization.dll): XmlDictionary, XmlDictionaryWriter e XmlDictionaryReader. Como o padrão XML utilizado pelo WCF possue várias características específicas, essas classes foram desenhadas exclusivamente para transformar a classe Message em um encoding específico (Text, Binary e MTOM).

Como o próprio nome diz, a classe XmlDictionary é um dicionário (chave/valor), utilizado pelo WCF para reduzir o tamanho das tags XML (não do conteúdo) que, consequentemente, irá gerar uma mensagem menor. Isso obrigará ambos os lados compartilharem o mesmo dicionário, para conseguir interpretar a mensagem.

A classe XmlDictionaryWriter trabalha diretamente com a serialização e codificação da classe Message. Ela herda da classe XmlWriter, mas ainda continua sendo uma classe abstrata. Essa classe define três métodos estáticos que recebem um stream como parâmetro (que servirá como output) e retornam as implementações concretas desta classe, sendo: CreateTextWriter, CreateBinaryWriter e CreateMtomWriter. Cada um desses métodos retornará uma classe que herda direta ou indiretamente da classe XmlDictionaryWriter, correspondendo as codificações suportadas pelo WCF, que por padrão são: Text, Binary ou MTOM, respectivamente. Ainda há um método estático chamado CreateDictionaryWriter, que serve como um facilitador quando temos um objeto do tipo XmlWriter e queremos “transformá-lo” em XmlDictionaryWriter.

De forma semelhante o XmlDictionaryWriter trabalha o XmlDictionaryReader. O XmlDictionaryReader herda diretamente da classe XmlReader, e serve como base para as classes que são responsáveis por ler o conteúdo serializado e codificado em XML. Esta classe também fornece métodos estáticos que retornam as implementações concretas de cada leitor, sendo: CreateTextReader, CreateBinaryReader, CreateMtomReader e CreateDictionaryReader. Esses métodos recebem como parâmetro um stream ou array de bytes representando o conteúdo codificado em um determinado tipo.

Além destas classes operarem com o formato de XML Documents, elas também podem manipular XML Infosets. O padrão Infosets permite expressar vários formatos de documentos XML (que possuem diferentes estruturas e regras de análise) em um formato único, definindo os elementos e atributos que o documento contém, de uma forma completamente diferente da sua representação. Isso torna tudo muito flexível, já que possibilita novos tipos de codificação. O WCF separa a serialização da codificação, o que faz com ele primeiramente serialize a classe Message em Infosets, e depois disso, codifica esses Infosets em um formato específico, utilizando os que já são fornecidos pela plataforma (Text, Binary ou MTOM), ou criando um customizado. Esse padrão também influencia no envelope SOAP gerado pelo WCF, mas vamos discutir isso mais adiante.

O exemplo a seguir mostra como podemos proceder para testar o funcionamento e analisar a geração de cada um dos tipos que vimos acima. Note que estou utilizando o método CreateMtomWriter, mas poderia ser qualquer um dos outros métodos, obviamente, alterando alguns parâmetros na chamada, de acordo com a exigência de cada um deles. Neste exemplo, ele irá serializar tudo o que estamos digitando em XML Infosets, e em seguida, codificar no padrão MTOM.

using (FileStream fs = new FileStream("Dados.mtom", FileMode.Create))
{
using (XmlDictionaryWriter writer =
XmlDictionaryWriter.CreateMtomWriter(fs, Encoding.UTF8, 1024, "application/xop+xml"))
{
writer.WriteStartElement("teste");
writer.WriteString("algum valor");
writer.WriteEndElement();
}
}

De volta a classe Message, ela está fortemente vinculada a uma das versões do protocolo SOAP existente no mercado. Como já foi falado acima, assim como o envelope SOAP, a classe Message também possui as seções que determinam os headers e o body. As regras para ler e escrever os dados no body e nos headers são diferentes, por exemplo, os headers sempre serão buferizados na memória e podem ser acessados em qualquer ordem e quantas vezes desejar, enquanto o body pode somente ser lido uma única vez e pode ser streamed. Para maiores informações sobre a comparação entre buffered e streamed, consulte este artigo.

Na maioria das vezes, a criação da classe Message é feita pelo próprio WCF. Em alguns cenários, você pode utilizar alguns pontos de extensibilidade para interceptar a mensagem e, consequentemente, extrair e/ou gravar informações nesta classe. Também temos a possibilidade de confeccionar um contrato que aceite ou devolva instâncias das classe Message, e quando isso acontece, há algumas restrições, como por exemplo:

  • A operação não pode ter qualquer parâmetro de saída (out) ou de referência (ref).
  • Não pode haver mais do que um parâmetro. Se houver um parâmetro, ele deve ser do tipo Message ou ter o ser um contrato de mensagem.
  • O retorno de ser void, Message ou um contrato de mensagem.

Para iniciar, vamos analisar as – poucas – propriedades que essa classe fornece. Para começar, vamos analisar as propriedades Headers e Properties. A prmieira delas representa a coleção de headers existentes em uma mensagem e influenciarão no processamento dela, já que podem armazenar informações de correlação, transações, segurança, mensagens confiáveis, etc., tudo de acordo com as especificações WS-*, ou seja, esses headers são utilizados pela própria infraestrutura do WCF e ultrapassam possíveis intermediários, chegando até o seu destino final. Já a propriedade Properties expõe um dicionário de dados, e essas informações são utilizadas "localmente", não ultrapassando possíveis intermediários. O próprio WCF já utiliza isso em alguns casos como, por exemplo, nos protocolos existentes e suportados por ele. Caso o transporte seja realizado através do protocolo HTTP, os detalhes específicos da requisição/protocolo (HTTP Headers) são armazenados neste dicionário, "fora" da mensagem.

Há duas propriedades públicas boleanas, chamada de IsEmpty e IsFault. A primeira delas, indica se a mensagem está ou não vazia, enquanto a segunda, define se a mensagem é ou não uma mensagem que descreve uma falha (Fault).

Como o body da mensagem é um stream, ele pode ser lido ou escrito apenas uma única vez. Para assegurar esse comportamento, o WCF disponibiliza uma propriedade de somente-leitura chamada State. Essa propriedade retorna um dos valores definidos no enumerador MessageState: Created, Read, Written, Copied e Closed. O valor dessa propriedade vai alterando de acordo com o métodos de escrita ou leitura que você invoca a partir da instância.

Por último e não menos importante, temos a propriedade Version. O valor dessa propriedade é definida no momento da criação da mensagem e não pode ser mais alterada. Essa propriedade contém informações à respeito de qual versão do envelope SOAP e o protocolo de endereçamento (WS-Addressing) que será usado pela mensagem.

Atualmente temos duas versões de envelope SOAP: 1.1 e 1.2. Como falamos acima, a escolha do tipo de envelope traz consequências durante a criação da mensagem, ou seja, a versão 1.1 (que é a mais utilizada) é baseada na sintaxe XML, enquanto a versão 1.2 faz uso de XML Infosets.

Já o WS-Addressing é uma especificação que define um mecanismo para permitir o endereçamento e roteamento de mensagens, independentemente de qual protocolo esteja utilizando. Além de definir a estrutura de um endpoint, o WS-Addressing também adiciona vários headers no envelope SOAP, definindo para onde a mensagem está indo, como reagir a esta mensagem e como as mensagens de requisição/resposta estão vinculadas.

Para representar qual versão do SOAP e do WS-Addressing a mensagem vai utilizar, o WCF disponibiliza uma classe chamada MessageVersion. Em seu construtor, você deverá especificar a versão do SOAP e do WS-Addressing que ela irá representar. Para expor qual versão do SOAP e WS-Addressing que a instância da classe MessageVersion está armazenando, ela define duas propriedades de instância e de somente-leitura chamada Envelope (do tipo EnvelopeVersion) e Addressing (do tipo AddressingVersion).

Tanto a classe EnvelopeVersion quanto a AddressingVersion possui propriedades estáticas que retornam a instância dela mesma, pré-configurada com a versões suportadas do SOAP e WS-Addressing. Ainda possuem uma propriedade chamada None, para caso onde não se aplica o SOAP (como é o caso do AJAX/JSON) ou o WS-Addressing. Para ter uma melhor reusabilidade, a classe MessageVersion também possui várias propriedades estáticas (Default, None, Soap11, Soap11WSAddressing10, Soap11WSAddressingAugust2004, Soap12, Soap12WSAddressing10, Soap12WSAddressingAugust2004) que retornam instâncias dela mesma, pré-configurada com as combinações suportadas/mais utilizadas.

A classe OperationContext fornece uma propriedade chamada IncomingMessageVersion, que retorna uma instância da classe MessageVersion, para obter a versão da mensagem que está chegando até o serviço/cliente. Você pode utilizar essa propriedade para criar uma nova mensagem baseando-se na mesma versão da outra parte.

Criando a classe Message

Você não pode criar diretamente a instância da classe Message, pois ela não tem um construtor público. Ao invés disso, ela fornece um método estático chamado CreateMessage que, por sua vez, fornece vários overloads. Obrigatoriamente os overloads deste método recebem, entre os vários parâmetros, uma instância da classe MessageVersion, especificando qual a versão de SOAP e do WS-Addressing que aquela mensagem utilizará. Grande parte destes overloads também recebem uma string, representando a Action da mensagem, informação que o WCF confia e a utiliza para determinar para qual método ele deverá entregar a mensagem.

O overload mais simples recebe apenas a versão e a action, criando uma mensagem com o corpo vazio. Já outro overload recebe também um object, que cria a mensagem serializando aquele objeto como corpo da mensagem. Nestes casos, mensagem utilizará o DataContractSerializer com as configurações padrão para efetuar a serialização do objeto. Caso desejar mudar isso, você pode utilizar um overload específico, que recebe como parâmetro um objeto do tipo XmlObjectSerializer.

O código abaixo ilustra um exemplo simples de criação da classe Message. A finalidade do código é criar uma mensagem que armazena a instância da classe Usuario que possui apenas uma propriedade pública chamada Nome. Inicialmente criamos a instância da classe Message através do overload do método CreateMessage, que recebe a versão (SOAP e WS-Addressing), uma Action e o body. A instância da classe XmlDictionaryWriter recebe em seu construtor um objeto do tipo FileStream, para que o resultado seja armazenado em um arquivo físico. Na sequência, invocamos o método WriteMessage a partir da classe Message, passando a instância do XmlDictionaryWriter, que serializará a mensagem neste objeto.

using (Message msg = Message.CreateMessage(
MessageVersion.Default, "Cadastrar", new Usuario() { Nome = "Israel" }))
{
Console.WriteLine(msg.State); //Created

using (FileStream fs = new FileStream("Message.xml", FileMode.Create))
using (XmlDictionaryWriter xml = XmlDictionaryWriter.CreateTextWriter(fs))
msg.WriteMessage(xml);

Console.WriteLine(msg.State); //Written
}

O método WriteMessage é um dos métodos de escrita que a classe Message disponibiliza. Há outras versões que nos dará um maior controle em como as “partes” da mensagem são escritas. Esses métodos autoexplicativos são: WriteBody, WriteBodyContents, WriteStartBody, WriteStartEnvelope e WriteStartHeaders e todos eles recebem como parâmetro uma instância da classe XmlDictionaryWriter.

Lendo a classe Message

Um dos overloads do método CreateMessage recebe uma instância da classe XmlDictionaryReader. Esse overload é utilizado quando queremos fazer o processo inverso, ou seja, temos a mensagem serializada em algum local (como um arquivo no disco), e desejamos transformá-la novamente em uma classe Message. Além do XmlDictionaryReader, ainda precisamos informar ao método CreateMessage um número inteiro e uma instância da classe MessageVersion.

O número inteiro permite controlar o tamanho máximo do header da mensagem, já que ele é buferizado. A versão da mensagem deve sempre refletir a mesma versão usada quando ela foi serializada. O código abaixo ilustra como podemos recuperar o conteúdo que foi salvo no arquivo “Message.xml” (através do exemplo anterior), e transformá-lo novamente em uma instância da classe Message. Note que utilizamos o método CreateTextReader para criar o XmlDictionaryReader, seguindo o mesmo exemplo acima:

using (FileStream fs = new FileStream("Message.xml", FileMode.Open))
{
using (XmlDictionaryReader xml =
XmlDictionaryReader.CreateTextReader(fs, XmlDictionaryReaderQuotas.Max))
{
using (Message msg = Message.CreateMessage(xml, 1024, MessageVersion.Default))
{
Usuario u = msg.GetBody<Usuario>();
}
}
}

Uma vez criado a instância da classe Message, precisamos saber como devemos proceder para extrair o seu conteúdo, ou melhor, extrair o que está contido no body da mensagem. Basicamente temos dois métodos: GetBody<T> e GetReaderAtBodyContents. O primeiro deles, recebe um parâmetro genérico, utilizado pelo mesmo para deserializar o body da mensagem neste tipo especificado, utilizando o DataContractSerializer. Há um overload deste método que aceita uma instância da classe XmlObjectSerializer, que te permitirá customizar o mecanismo de deserialização. Já o método GetReaderAtBodyContents retorna um novo XmlDictionaryReader, posicionado no elemento body do envelope SOAP.

Independentemente de qual dos métodos utilize para ler o body da mensagem, você poderá chamar apenas uma única vez. Chamando duas vezes um dos métodos de leitura, uma exceção do tipo InvalidOperationException será disparada. Em situações onde você precisa processar o body múltiplas vezes, então você precisará criar uma cópia buferizada da mensagem. Para isso, a classe Message fornece um método chamado CreateBufferedCopy que retorna uma instância da classe MessageBuffer. A instância dessa classe representa a mensagem em memória, disponibilizando um método chamado CreateMessage, que retorna uma cópia idêntica da mensagem original. O exemplo abaixo ilustra o seu uso:

using (Message msg = Message.CreateMessage(xml, 1024, MessageVersion.Default))
{
MessageBuffer mb = msg.CreateBufferedCopy(int.MaxValue);
Console.WriteLine(mb.CreateMessage().GetBody<Usuario>().Nome);
Console.WriteLine(mb.CreateMessage().GetBody<Usuario>().Nome);
}

Criando Fault Messages

O método CreateMessage ainda possui dois overloads que permitem a criação de uma instância da classe Message que representa uma mensagem de falha. Um desses overloads aceita como parâmetro uma instância da classe FaultCode e o outro overload recebe uma instância da classe MessageFault. O código abaixo ilustra a criação de uma MessageFault, e em seguida utilizamos o método CreateMessage passando esta fault criada:

MessageFault mf =
MessageFault.CreateFault(new FaultCode("Receiver"), new FaultReason("Dados inválidos"));

using (Message msg = Message.CreateMessage(MessageVersion.Default, mf, "Cadastrar"))
{
//…
}

Ao chamar a propriedade IsFault em cima da instância da classe Message criada acima, o valor retornado será True. Ao visualizar o resultado gerado pelo código acima, teremos:

<s:Envelope
xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:a="http://www.w3.org/2005/08/addressing">
<s:Header>
<a:Action s:mustUnderstand="1">Cadastrar</a:Action>
</s:Header>
<s:Body>
<s:Fault>
<s:Code>
<s:Value>s:Receiver</s:Value>
</s:Code>
<s:Reason>
<s:Text xml:lang="en-US">Dados inválidos</s:Text>
</s:Reason>
</s:Fault>
</s:Body>
</s:Envelope>

Headers e Properties

A propriedade Headers é uma coleção, enquanto a propriedade Properties é um dicionário e podem ser manipuladas através de métodos como Add, Insert, Remove, etc. Para maiores detalhes sobre a finalidade de cada uma dessas propriedades, consulte este artigo.

Conclusão: Neste artigo vimos como podemos proceder para a criação, leitura e manipulação de uma mensagem, que é utilizada por serviços WCF. É importante conhecer esses detalhes, principalmente quando você desejar interceptar algum ponto durante a execução do serviço. Além disso, é provável que em algum momento você precisará customizar a criação e/ou leitura da mensagem, e para isso, você deverá manipular instâncias da classe Message, ao invés de deixar o WCF criá-las.

Israel Aéce

Israel Aéce - Especialista em tecnologias de desenvolvimento Microsoft, atua como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. Como instrutor Microsoft, leciona sobre o desenvolvimento de aplicações .NET. É palestrante em diversos eventos Microsoft no Brasil e autor de diversos artigos que podem ser lidos a partir de seu site http://www.israelaece.com/. Possui as seguintes credenciais: MVP (Connected System Developer), MCP, MCAD, MCTS (Web, Windows, Distributed, ASP.NET 3.5, ADO.NET 3.5, Windows Forms 3.5 e WCF), MCPD (Web, Windows, Enterprise, ASP.NET 3.5 e Windows 3.5) e MCT.