Visão geral da arquitetura de transferência de dados
O WCF (Windows Communication Foundation) pode ser pensado como uma infraestrutura de mensagens. Ele pode receber mensagens, processá-las e expedi-las ao código do usuário para outras ações, ou pode criar mensagens com dados fornecidos pelo código do usuário e entregá-las a um destino. Este tópico, destinado a desenvolvedores avançados, descreve a arquitetura para lidar com mensagens e os dados contidos nelas. Para obter uma exibição mais simples e orientada a tarefas de como enviar e receber dados, confira Especificando a transferência de dados em contratos de serviço.
Observação
Este tópico discute detalhes da implementação do WCF que não ficam visíveis com o exame do modelo de objeto WCF. Dois cuidados são necessários em relação aos detalhes de implementação documentados. Primeiro, as descrições são simplificadas; a implementação real pode ser mais complexa devido a otimizações ou outros motivos. Em segundo lugar, você nunca deve confiar em detalhes de implementação específicos, mesmo documentados, pois eles podem ser alterados sem aviso prévio de versão para versão ou mesmo em uma versão de manutenção.
Arquitetura básica
No núcleo dos recursos de tratamento de mensagens do WCF está a classe Message, que é descrita em detalhes em Usando a classe de mensagens. Os componentes de tempo de execução do WCF podem ser divididos em duas partes principais: a pilha de canais e a estrutura de serviço, sendo a classeMessage o ponto de conexão.
A pilha de canais é responsável pela conversão entre uma instância Message válida e alguma ação que corresponde ao envio ou recebimento de dados de mensagem. No lado do envio, a pilha de canais usa uma instância válida Message e, após algum processamento, executa ação que corresponde logicamente ao envio da mensagem. A ação pode estar enviando pacotes TCP ou HTTP, enfileirando a mensagem no Enfileiramento de Mensagens, escrevendo a mensagem em um banco de dados, salvando-a em um compartilhamento de arquivos ou em qualquer outra ação, dependendo da implementação. A ação mais comum é enviar a mensagem por meio de um protocolo de rede. No lado de recebimento, ocorre o oposto: uma ação é detectada (que pode ser pacotes TCP ou HTTP chegando ou outra ação) e, após o processamento, o a pilha de canais converte essa ação em uma instância Message válida.
Você pode usar o WCF usando a classe Message e a pilha de canais diretamente. No entanto, fazer isso é difícil e demorado. Além disso, o objeto Message não dá suporte a metadados, ou seja, você não poderá gerar clientes WCF fortemente tipados se usar o WCF dessa maneira.
Portanto, o WCF inclui uma estrutura de serviço que fornece um modelo de programação fácil de usar que você pode utilizar para construir e receber objetos Message
. A estrutura de serviço mapeia serviços para tipos .NET Framework por meio da noção de contratos de serviço e envia mensagens para operações de usuário que são simplesmente métodos do .NET Framework marcados com o atributo OperationContractAttribute (para obter mais detalhes, confira Design de contratos de serviço). Esses métodos podem ter parâmetros e valores retornados. No lado do serviço, a estrutura de serviço converte instâncias de entrada Message em parâmetros e converte valores retornados em instâncias de saída Message. No lado do cliente, ele faz o oposto. Por exemplo, considere a operação FindAirfare
abaixo.
[ServiceContract]
public interface IAirfareFinderService
{
[OperationContract]
int FindAirfare(string FromCity, string ToCity, out bool IsDirectFlight);
}
<ServiceContract()> _
Public Interface IAirfareFinderService
<OperationContract()> _
Function FindAirfare(ByVal FromCity As String, _
ByVal ToCity As String, ByRef IsDirectFlight As Boolean) As Integer
End Interface
Suponha que FindAirfare
seja chamado no cliente. A estrutura de serviço no cliente converte os parâmetros FromCity
e ToCity
em uma instância de saída Message e a transmite à pilha de canais a ser enviada.
No lado do serviço, quando uma instância Message chega da pilha de canais, a estrutura de serviço extrai os dados relevantes da mensagem para preencher os parâmetros FromCity
e ToCity
e chama o método FindAirfare
do lado do serviço. Quando o método retorna, a estrutura de serviço usa o valor inteiro retornado e o parâmetro de saída IsDirectFlight
e cria uma instância de objeto Message que contém essas informações. Em seguida, ele transmite a instância Message
à pilha de canais a ser enviada de volta para o cliente.
No lado do cliente, uma instância Message que contém a mensagem de resposta surge da pilha de canais. A estrutura de serviço extrai o valor retornado e o valor IsDirectFlight
e retorna-os ao chamador do cliente.
Classe de mensagens
A classe Message serve como representação abstrata de uma mensagem, mas seu design está bastante vinculado à mensagem SOAP. Uma Message contém três informações principais: corpo, cabeçalhos e propriedades de mensagem.
Corpo da mensagem
O corpo da mensagem destina-se a representar a carga de dados real da mensagem. O corpo da mensagem sempre é representado como um Infoset XML. Isso não significa que todas as mensagens criadas ou recebidas no WCF precisem estar no formato XML. Cabe à pilha de canais decidir como interpretar o corpo da mensagem. Ele pode emiti-lo como XML, convertê-lo em algum outro formato ou até mesmo omiti-lo inteiramente. É claro que, com a maioria das associações fornecidas pelo WCF, o corpo da mensagem é representado como conteúdo XML na seção do corpo de um envelope SOAP.
É importante perceber que a classe Message
não contém necessariamente um buffer com dados XML que representam o corpo. Logicamente, Message
contém um Infoset XML, mas esse Infoset pode ser construído dinamicamente e nunca existir fisicamente na memória.
Colocando dados no corpo da mensagem
Não há nenhum mecanismo uniforme para colocar dados em um corpo de mensagem. A classe Message tem um método abstrato, OnWriteBodyContents(XmlDictionaryWriter), que usa um XmlDictionaryWriter. Cada subclasse da classe Message é responsável por substituir esse método e escrever seu próprio conteúdo. O corpo da mensagem contém logicamente o Infoset XML que OnWriteBodyContent
produz. Por exemplo, considere a subclasse Message
a seguir.
public class AirfareRequestMessage : Message
{
public string fromCity = "Tokyo";
public string toCity = "London";
//code omitted…
protected override void OnWriteBodyContents(XmlDictionaryWriter w)
{
w.WriteStartElement("airfareRequest");
w.WriteElementString("from", fromCity);
w.WriteElementString("to", toCity);
w.WriteEndElement();
}
public override MessageVersion Version
{
get { throw new NotImplementedException("The method is not implemented.") ; }
}
public override MessageProperties Properties
{
get { throw new Exception("The method or operation is not implemented."); }
}
public override MessageHeaders Headers
{
get { throw new Exception("The method or operation is not implemented."); }
}
public override bool IsEmpty
{
get
{
return base.IsEmpty;
}
}
public override bool IsFault
{
get
{
return base.IsFault;
}
}
}
Public Class AirfareRequestMessage
Inherits Message
Public fromCity As String = "Tokyo"
Public toCity As String = "London"
' Code omitted…
Protected Overrides Sub OnWriteBodyContents(ByVal w As XmlDictionaryWriter)
w.WriteStartElement("airfareRequest")
w.WriteElementString("from", fromCity)
w.WriteElementString("to", toCity)
w.WriteEndElement()
End Sub
Public Overrides ReadOnly Property Version() As MessageVersion
Get
Throw New NotImplementedException("The method is not implemented.")
End Get
End Property
Public Overrides ReadOnly Property Properties() As MessageProperties
Get
Throw New Exception("The method or operation is not implemented.")
End Get
End Property
Public Overrides ReadOnly Property Headers() As MessageHeaders
Get
Throw New Exception("The method or operation is not implemented.")
End Get
End Property
Public Overrides ReadOnly Property IsEmpty() As Boolean
Get
Return MyBase.IsEmpty
End Get
End Property
Public Overrides ReadOnly Property IsFault() As Boolean
Get
Return MyBase.IsFault
End Get
End Property
End Class
Fisicamente, uma instância AirfareRequestMessage
contém apenas duas cadeias de caracteres ("fromCity" e "toCity"). No entanto, logicamente, a mensagem contém o seguinte infoset XML:
<airfareRequest>
<from>Tokyo</from>
<to>London</to>
</airfareRequest>
É claro que normalmente você não criaria mensagens dessa maneira, pois pode usar a estrutura de serviço para criar uma mensagem como a anterior dos parâmetros de contrato de operação. Além disso, a classe Message tem métodos estáticos CreateMessage
que você pode usar para criar mensagens com tipos comuns de conteúdo: uma mensagem vazia, uma mensagem que contém um objeto serializado para XML com a DataContractSerializer, uma mensagem que contém uma falha SOAP, uma mensagem que contém XML representado por um XmlReader e assim por diante.
Obtendo dados de um corpo de mensagem
Você pode extrair os dados armazenados em um corpo de mensagem de duas maneiras principais:
Você pode obter todo o corpo da mensagem ao mesmo tempo chamando o método WriteBodyContents(XmlDictionaryWriter) e transmitindo um gravador XML. O corpo completo da mensagem é registrado nesse gravador. Obter todo o corpo da mensagem ao mesmo tempo também é chamado de gravação de mensagem. A gravação é feita principalmente pela pilha de canais na hora de enviar mensagens; parte da pilha de canais normalmente obterá acesso a todo o corpo da mensagem, o codificará e o enviará.
Outra maneira de obter informações do corpo da mensagem é chamar GetReaderAtBodyContents() e obter um leitor XML. O corpo da mensagem pode ser acessado sequencialmente, conforme a necessidade, com a chamada de métodos no leitor. Obter o corpo da mensagem peça por peça também é chamado de leitura de mensagem. A leitura da mensagem é usada principalmente pela estrutura de serviço no recebimento de mensagens. Por exemplo, quando DataContractSerializer estiver em uso, a estrutura de serviço obterá um leitor XML pelo corpo e o transmitirá ao mecanismo de desserialização, que começará a ler a mensagem elemento a elemento e a construir o grafo de objeto correspondente.
Um corpo de mensagem pode ser recuperado apenas uma vez. Isso possibilita trabalhar com fluxos somente de encaminhamento. Por exemplo, você pode escrever uma gravar uma substituição de OnWriteBodyContents(XmlDictionaryWriter) que lê de um FileStream e retorna os resultados como um Infoset XML. Você nunca precisará "retroceder" para o início do arquivo.
Os métodos WriteBodyContents
e GetReaderAtBodyContents
simplesmente verificam se o corpo da mensagem nunca foi recuperado antes e chamam OnWriteBodyContents
ou OnGetReaderAtBodyContents
, respectivamente.
Uso de mensagem no WCF
A maioria das mensagens pode ser classificada como de saída (aquelas criadas pela estrutura de serviço a serem enviadas pela pilha de canais) ou entrada (aquelas que chegam da pilha de canais e são interpretadas pela estrutura de serviço). Além disso, a pilha de canais pode operar no modo buffer ou streaming. A estrutura de serviço também pode expor um modelo de programação transmitido ou não transmitido. Isso leva aos casos listados na tabela a seguir, juntamente com detalhes simplificados de sua implementação.
Tipo de mensagem | Dados do corpo na mensagem | Implementação de gravação (OnWriteBodyContents) | Implementação de leitura (OnGetReaderAtBodyContents) |
---|---|---|---|
Saída, criada com o modelo de programação não transmitido | Os dados necessários para gravar a mensagem (por exemplo, um objeto e a instância DataContractSerializer necessárias para serializá-lo)* | Lógica personalizada para gravar a mensagem com base nos dados armazenados (por exemplo, chame WriteObject em DataContractSerializer se esse for o serializador em uso)* |
Chamar OnWriteBodyContents , armazenar os resultados em buffer, retornar um leitor XML pelo buffer |
Saída, criada com o modelo de programação transmitido | O Stream com os dados a serem gravados* |
Gravar dados do fluxo armazenado usando o mecanismo IStreamProvider* | Chamar OnWriteBodyContents , armazenar os resultados em buffer, retornar um leitor XML pelo buffer |
Entrada da pilha de canais de streaming | Um objeto Stream que representa os dados que chegam pela rede com um XmlReader sobre ele |
Gravar o conteúdo do XmlReader armazenado usando WriteNode |
Retorna o XmlReader armazenado |
Entrada da pilha de canais não transmitida | Um buffer que contém dados do corpo com um XmlReader sobre ele |
Gravar o conteúdo do XmlReader armazenado usando WriteNode |
Retorna o lang armazenado |
* Esses itens não são implementados diretamente em subclasses Message
, mas em subclasses da classe BodyWriter. Para saber mais sobre o BodyWriter, confira Usando a classe de mensagens.
Cabeçalhos da mensagem
Uma mensagem pode conter cabeçalhos. Um cabeçalho consiste logicamente em um Infoset XML associado a um nome, um namespace e algumas outras propriedades. Os cabeçalhos de mensagem são acessados usando a propriedade Headers
em Message. Cada cabeçalho é representado por uma classe MessageHeader. Normalmente, os cabeçalhos de mensagem são mapeados para cabeçalhos de mensagem SOAP durante o uso de uma pilha de canais configurada para trabalhar com mensagens SOAP.
A colocação de informações em um cabeçalho de mensagem e a extração de informações dela são semelhantes ao uso do corpo da mensagem. O processo é um pouco simplificado porque não há suporte a streaming. É possível acessar o conteúdo do mesmo cabeçalho mais de uma vez, e os cabeçalhos podem ser acessados em ordem aleatória, forçando os cabeçalhos a serem sempre armazenados em buffer. Não há nenhum mecanismo de uso geral disponível para obter um leitor XML sobre um cabeçalho, mas há uma subclasse MessageHeader
interna do WCF que representa um cabeçalho legível com essa funcionalidade. Esse tipo de MessageHeader
é criado pela pilha de canais quando entra uma mensagem com cabeçalhos de aplicativo personalizados. Isso permite que a estrutura de serviço use um mecanismo de desserialização, como o DataContractSerializer, para interpretar esses cabeçalhos.
Para saber mais, confira Usando a classe de mensagens.
Propriedades da Mensagem
Uma mensagem pode conter propriedades. Uma propriedade é qualquer objeto .NET Framework associado a um nome de cadeia de caracteres. As propriedades são acessadas por meio da propriedade Properties
em Message
.
Ao contrário dos cabeçalhos e corpo da mensagem (que normalmente são mapeados para o corpo SOAP e cabeçalhos SOAP, respectivamente), as propriedades da mensagem normalmente não são enviadas ou recebidas junto com as mensagens. As propriedades da mensagem existem principalmente como um mecanismo de comunicação para passar dados sobre a mensagem entre os vários canais na pilha de canais e entre a pilha de canais e o modelo de serviço.
Por exemplo, o canal de transporte HTTP incluído como parte do WCF é capaz de produzir vários códigos de status HTTP, como "404 (Não encontrado)" e "500 (Erro interno do servidor)" quando ele envia respostas aos clientes. Antes de enviar uma mensagem de resposta, ele verifica se a propriedade Properties
Message
contém uma propriedade chamada "httpResponse" com um objeto do tipo HttpResponseMessageProperty. Se essa propriedade for encontrada, ela examinará a propriedade StatusCode e usará esse código de status. Se não for encontrado, o código padrão "200 (OK)" será usado.
Para saber mais, confira Usando a classe de mensagens.
A mensagem como um todo
Até agora, discutimos métodos para acessar as várias partes da mensagem isoladamente. No entanto, a classe Message também fornece métodos para trabalhar com toda a mensagem como um todo. Por exemplo, o método WriteMessage
grava toda a mensagem em um gravador XML.
Para que isso seja possível, um mapeamento precisa ser definido entre toda a instância Message
e um Infoset XML. Esse mapeamento, na verdade, existe: o WCF usa o padrão SOAP para definir esse mapeamento. Quando uma instância Message
é gravada como um Infoset XML, o Infoset resultante é o envelope SOAP válido que contém a mensagem. Assim, WriteMessage
normalmente realizaria as seguintes etapas:
Escrever a marca de abertura do elemento envelope SOAP.
Escrever a marca de abertura do elemento de cabeçalho SOAP, escrever todos os cabeçalhos e fechar o elemento cabeçalho.
Escrever a marca de abertura do elemento corpo SOAP.
Chamar
WriteBodyContents
ou um método equivalente para gravar o corpo.Fechar os elementos corpo e envelope.
As etapas anteriores estão intimamente vinculadas ao padrão SOAP. Isso é complicado pelo fato de que existem várias versões de SOAP, por exemplo, é impossível gravar o elemento envelope SOAP corretamente sem conhecer a versão SOAP em uso. Além disso, em alguns casos, pode ser desejável desativar completamente esse mapeamento específico de SOAP complexo.
Para isso, uma propriedade Version
é fornecida em Message
. Ela pode ser definida como a versão SOAP a ser usada ao gravar a mensagem ou pode ser definida como None
para evitar mapeamentos específicos de SOAP. Se a propriedade Version
for definida como None
, os métodos que funcionam com a mensagem inteira atuam como se a mensagem consistisse apenas em seu corpo, por exemplo, WriteMessage
simplesmente chamaria WriteBodyContents
em vez de executar as várias etapas listadas acima. Espera-se que, em mensagens de entrada, Version
seja detectada automaticamente e definida corretamente.
A pilha de canais
Canais
Conforme indicado anteriormente, a pilha de canais é responsável por converter instâncias de saída Message em alguma ação (como enviar pacotes pela rede) ou converter alguma ação (como receber pacotes de rede) em instâncias Message
de entrada.
A pilha de canais é composta por um ou mais canais ordenados em uma sequência. Uma instância Message
de saída é transmitida ao primeiro canal na pilha (também chamado de canal superior), que a transmite ao próximo canal para baixo na pilha e assim por diante. A mensagem termina no último canal, que é chamado de canal de transporte. As mensagens de entrada se originam no canal de transporte e são transmitidas de canal para canal pilha acima. No canal mais alto, a mensagem geralmente é transmitida à estrutura de serviço. Embora esse seja o padrão usual para mensagens de aplicativo, alguns canais podem funcionar de forma ligeiramente diferente, por exemplo, eles podem enviar suas próprias mensagens de infraestrutura sem transmitir uma mensagem de um canal acima.
Os canais podem operar na mensagem de várias maneiras conforme ela passa pela pilha. A operação mais comum é adicionar um cabeçalho a uma mensagem de saída e ler cabeçalhos em uma mensagem de entrada. Por exemplo, um canal pode calcular a assinatura digital de uma mensagem e adicioná-la como um cabeçalho. Um canal também pode inspecionar esse cabeçalho de assinatura digital em mensagens de entrada e bloquear mensagens que não têm uma assinatura válida para que não subam a pilha de canais. Os canais também geralmente definem ou inspecionam as propriedades da mensagem. O corpo da mensagem geralmente não é modificado, embora isso seja permitido, por exemplo, o canal de segurança do WCF pode criptografar o corpo da mensagem.
Canais de transporte e codificadores de mensagens
O canal mais abaixo na pilha é responsável por realmente transformar uma Message de saída, conforme modificado por outros canais, em alguma ação. No lado do recebimento, esse é o canal que converte alguma ação em um Message
processada por outros canais.
Conforme indicado anteriormente, as ações podem ser variadas: enviar ou receber pacotes de rede em vários protocolos, ler ou gravar a mensagem em um banco de dados ou enfileirar ou desenfileirar a mensagem em uma fila de Enfileiramento de Mensagens, para dar apenas alguns exemplos. Todas essas ações têm uma coisa em comum: elas exigem uma transformação entre a instância do WCF Message
e um grupo real de bytes que podem ser enviados, recebidos, lidos, gravados, enfileirados ou desenfileirados. O processo de conversão de uma Message
em um grupo de bytes é chamado de codificação, e o processo inverso de criação de uma Message
de um grupo de bytes é chamado de decodificação.
A maioria dos canais de transporte usa componentes chamados codificadores de mensagens para realizar o trabalho de codificação e decodificação. Um codificador de mensagens é uma subclasse da classe MessageEncoder. MessageEncoder
inclui várias sobrecargas de método ReadMessage
e WriteMessage
para converter entre Message
e grupos de bytes.
No lado de envio, um canal de transporte de buffer transmite o objeto Message
que recebeu de um canal acima dele a WriteMessage
. Ele obtém de volta uma matriz de bytes que usa para executar sua ação (como empacotar esses bytes como pacotes TCP válidos e enviá-los para o destino correto). Um canal de transporte de streaming primeiro cria um Stream
(por exemplo, sobre a conexão TCP de saída) e, em seguida, transmite Stream
e Message
que ele precisa enviar para a sobrecarga de WriteMessage
apropriada, que grava a mensagem.
No lado receptor, um canal de transporte de buffer extrai bytes de entrada (por exemplo, de pacotes TCP de entrada) em uma matriz e chama ReadMessage
para obter um objeto Message
que ele pode transmitir ainda mais acima na pilha de canais. Um canal de transporte de streaming cria um objeto Stream
(por exemplo, um fluxo de rede sobre a conexão TCP de entrada) e transmite isso a ReadMessage
para recuperar um objeto Message
.
A separação entre os canais de transporte e o codificador de mensagens não é obrigatória; é possível gravar um canal de transporte que não usa um codificador de mensagens. No entanto, a vantagem dessa separação é a facilidade de composição. Contanto que um canal de transporte use apenas o MessageEncoder base, ele pode funcionar com qualquer codificador de mensagens de terceiros ou do WCF. Da mesma forma, o mesmo codificador normalmente pode ser usado em qualquer canal de transporte.
Operação do codificador de mensagens
Para descrever a operação típica de um codificador, é útil considerar os quatro casos a seguir.
Operação | Comentário |
---|---|
Codificação, em buffer | No modo em buffer, o codificador normalmente cria um buffer de tamanho variável e cria um gravador XML sobre ele. Em seguida, ele chama WriteMessage(XmlWriter) sobre a mensagem que está sendo codificada, que grava os cabeçalhos e, em seguida, o corpo usando WriteBodyContents(XmlDictionaryWriter), conforme explicado na seção anterior sobre Message neste tópico. O conteúdo do buffer (representado como uma matriz de bytes) é retornado para o canal de transporte a ser usado. |
Codificação, transmitida | No modo transmitido, a operação é semelhante à que está acima, mas mais simples. Não há necessidade de um buffer. Um gravador XML normalmente é criado no fluxo e WriteMessage(XmlWriter) é chamado na Message a fim de gravá-lo nesse gravador. |
Decodificação, em buffer | Ao decodificar no modo em buffer, uma subclasse especial Message que contém os dados em buffer normalmente é criada. Os cabeçalhos da mensagem são lidos, e um leitor XML posicionado no corpo da mensagem é criado. Esse é o leitor que será retornado com GetReaderAtBodyContents(). |
Decodificação, transmitida | Ao decodificar no modo transmitido, uma subclasse de mensagem especial é normalmente criada. O fluxo é avançado apenas o suficiente para ler todos os cabeçalhos e posicioná-lo no corpo da mensagem. Em seguida, um leitor de XML é criado no fluxo. Esse é o leitor que será retornado com GetReaderAtBodyContents(). |
Os codificadores também podem executar outras funções. Por exemplo, os codificadores podem agrupar leitores e gravadores XML. É caro criar um novo leitor ou gravador XML sempre que for necessário. Portanto, os codificadores normalmente mantêm um pool de leitores e um pool de gravadores de tamanho configurável. Nas descrições da operação de codificador descritas anteriormente, sempre que a frase "criar um leitor/gravador XML" for usada, normalmente significa "tirar um do pool ou criar um se não estiver disponível". O codificador (e as subclasses Message
que ele cria durante a decodificação) contêm lógica para retornar leitores e gravadores aos pools depois que eles não são mais necessários (por exemplo, quando o Message
for fechado).
O WCF fornece três codificadores de mensagem, embora seja possível criar tipos personalizados adicionais. Os tipos fornecidos são MTOM (Mecanismo de Otimização de Transmissão de Mensagens), Binário e Texto. Eles são descritos em detalhes em Escolhendo um codificador de mensagens.
A interface IStreamProvider
Ao escrever uma mensagem de saída que contém um corpo transmitido para um gravador XML, Message usa uma sequência de chamadas semelhante ao seguinte em sua implementação OnWriteBodyContents(XmlDictionaryWriter):
Escreva as informações necessárias antes do fluxo (por exemplo, a marca XML de abertura).
Escreva o fluxo.
Escreva uma informação seguindo o fluxo (por exemplo, a marca XML de fechamento).
Isso funciona bem com codificações semelhantes à codificação XML textual. No entanto, algumas codificações não colocam informações do Infoset XML (por exemplo, marcas para elementos XML iniciais e finais) junto com os dados contidos nos elementos. Por exemplo, na codificação MTOM, a mensagem é dividida em várias partes. Uma parte contém o Infoset XML, que pode conter referências a outras partes de conteúdo de elemento real. O Infoset XML normalmente é pequeno em comparação com o conteúdo transmitido, ou seja, faz sentido armazenar em buffer o Infoset, gravá-lo e gravar o conteúdo de maneira transmitida. Isso significa que, quando a marca de elemento de fechamento for gravada, o fluxo ainda não deverá ter sido gravado.
Para essa finalidade, a interface IStreamProvider é usada. A interface tem um método GetStream() que retorna o fluxo a ser gravado. A maneira correta de gravar um corpo de mensagem transmitido em OnWriteBodyContents(XmlDictionaryWriter) é a seguinte:
Escreva as informações necessárias antes do fluxo (por exemplo, a marca XML de abertura).
Chame a sobrecarga
WriteValue
no XmlDictionaryWriter que leva um IStreamProvider, com uma implementaçãoIStreamProvider
que retorna o fluxo a ser gravado.Escreva uma informação seguindo o fluxo (por exemplo, a marca XML de fechamento).
Com essa abordagem, o gravador XML tem a opção de quando chamar GetStream() e gravar os dados transmitidos. Por exemplo, os gravadores XML textuais e binários o chamarão imediatamente e gravarão o conteúdo transmitido entre as marcas inicial e final. O gravador MTOM pode decidir chamar GetStream() mais tarde, quando estiver pronto para gravar a parte apropriada da mensagem.
Representando dados na estrutura de serviço
Conforme indicado na seção "Arquitetura Básica" deste tópico, a estrutura de serviço é a parte do WCF que, entre outras coisas, é responsável pela conversão entre um modelo de programação amigável para dados de mensagens e instâncias Message
reais. Normalmente, uma troca de mensagens é representada na estrutura de serviço como um método do .NET Framework marcado com o atributo OperationContractAttribute. O método pode usar alguns parâmetros e pode retornar um valor retornado ou parâmetros de saída (ou ambos). No lado do serviço, os parâmetros de entrada representam a mensagem de entrada e os parâmetros de retorno de valor e saída representam a mensagem de saída. No lado do cliente, o inverso é verdadeiro. O modelo de programação para descrever mensagens usando parâmetros e o valor retornado é descrito em detalhes em Especificando a transferência de dados em contratos de serviço. No entanto, esta seção fornecerá uma breve visão geral.
Modelos de programação
A estrutura de serviço do WCF dá suporte a cinco modelos de programação diferentes para descrever mensagens:
1. A mensagem vazia
Esse é o caso mais simples. Para descrever uma mensagem de entrada vazia, não use parâmetros de entrada.
[OperationContract]
int GetCurrentTemperature();
<OperationContract()> Function GetCurrentTemperature() As Integer
Para descrever uma mensagem de saída vazia, use um valor retornado nulo e não use parâmetros externos:
[OperationContract]
void SetDesiredTemperature(int t);
<OperationContract()> Sub SetDesiredTemperature(ByVal t As Integer)
Observe que isso é diferente de um contrato de operação unidirecional:
[OperationContract(IsOneWay = true)]
void SetLightbulb(bool isOn);
<OperationContract(IsOneWay:=True)> Sub SetLightbulb(ByVal isOn As Boolean)
No exemplo SetDesiredTemperature
, um padrão de troca de mensagens bidirecional é descrito. Uma mensagem é retornada da operação, mas está vazia. É possível retornar uma falha da operação. No exemplo "Definir Lâmpada", o padrão de troca de mensagens é unidirecional, ou seja, não há nenhuma mensagem de saída a ser descrita. O serviço não pode comunicar nenhum status de volta ao cliente nesse caso.
2. Usando a classe de mensagem diretamente
É possível usar a classe Message (ou uma de suas subclasses) diretamente em um contrato de operação. Nesse caso, a estrutura de serviço apenas transmite a Message
da operação à pilha de canais e vice-versa, sem processamento adicional.
Há dois casos principais de uso para usar Message
diretamente. Você pode usá-lo em cenários avançados, quando nenhum dos outros modelos de programação oferece flexibilidade suficiente para descrever sua mensagem. Por exemplo, você pode querer usar arquivos no disco para descrever uma mensagem, com as propriedades do arquivo se tornando cabeçalhos de mensagem e o conteúdo do arquivo se tornando o corpo da mensagem. Em seguida, você pode criar algo semelhante ao que segue.
public class FileMessage : Message
// Code not shown.
Public Class FileMessage
Inherits Message
' Code not shown.
O segundo uso comum de Message
em um contrato de operação é quando um serviço não se importa com o conteúdo específico da mensagem e atua na mensagem como em uma caixa preta. Por exemplo, você pode ter um serviço que encaminha mensagens para vários outros destinatários. O contrato pode ser gravado desta maneira.
[OperationContract]
public FileMessage GetFile()
{
//code omitted…
FileMessage fm = new FileMessage("myFile.xml");
return fm;
}
<OperationContract()> Public Function GetFile() As FileMessage
'code omitted…
Dim fm As New FileMessage("myFile.xml")
Return fm
End Function
A linha Action="*" desativa efetivamente o envio de mensagens e garante que todas as mensagens enviadas ao contrato IForwardingService
cheguem até a operação ForwardMessage
. (Normalmente, o dispatcher examinaria o cabeçalho “Action” da mensagem para determinar para qual operação ele serve. Action="*" significa “todos os valores possíveis do cabeçalho Action”.) A combinação de Action="*" e o uso de Message como parâmetro é conhecido como “contrato universal” porque é capaz de receber todas as mensagens possíveis. Para poder enviar todas as mensagens possíveis, use Message como o valor retornado e defina ReplyAction
como "*". Isso impedirá que a estrutura de serviço adicione seu próprio cabeçalho Action, permitindo que você controle esse cabeçalho usando o objeto Message
retornado.
3. Contratos de mensagem
O WCF fornece um modelo de programação declarativa para descrever mensagens, chamado contratos de mensagem. Esse modelo é descrito em detalhes em Usando contratos de mensagem. Essencialmente, toda a mensagem é representada por um único tipo de .NET Framework que usa atributos como MessageBodyMemberAttribute e MessageHeaderAttribute para descrever quais partes da classe de contrato de mensagem devem ser mapeadas para qual parte da mensagem.
Os contratos de mensagem fornecem muito controle sobre as instâncias resultantes Message
(embora obviamente não tenha o mesmo controle que usar a classe Message
diretamente). Por exemplo, os corpos das mensagens geralmente são compostos por várias informações, cada uma representada por seu próprio elemento XML. Esses elementos podem ocorrer diretamente no corpo (modobare ) ou podem ser encapsulados em um elemento XML abrangente. O uso do modelo de programação de contrato de mensagem permite que você tome a decisão bare-versus-wrapped e controle o nome do wrapper e do namespace.
O exemplo de código a seguir de um contrato de mensagem demonstra esses recursos.
[MessageContract(IsWrapped = true, WrapperName = "Order")]
public class SubmitOrderMessage
{
[MessageHeader]
public string customerID;
[MessageBodyMember]
public string item;
[MessageBodyMember]
public int quantity;
}
<MessageContract(IsWrapped:=True, WrapperName:="Order")> _
Public Class SubmitOrderMessage
<MessageHeader()> Public customerID As String
<MessageBodyMember()> Public item As String
<MessageBodyMember()> Public quantity As Integer
End Class
Os itens marcados para serem serializados (com MessageBodyMemberAttribute, MessageHeaderAttribute ou outros atributos relacionados) precisam ser serializáveis para participar de um contrato de mensagem. Para obter mais informações, confira a seção “Serialização” mais adiante neste tópico.
4. Parâmetros
Muitas vezes, um desenvolvedor que deseja descrever uma operação que atua em vários dados não precisa do grau de controle fornecido pelos contratos de mensagem. Por exemplo, durante a criação de serviços, geralmente não se deseja tomar a decisão bare-versus-wrapped e decidir sobre o nome do elemento wrapper. Tomar essas decisões geralmente requer um profundo conhecimento dos serviços Web e SOAP.
A estrutura de serviços do WCF pode escolher automaticamente a melhor representação SOAP e a mais interoperável para enviar ou receber várias informações relacionadas, sem exigir essas escolhas do usuário. Isso é feito simplesmente com a descrição dessas informações como parâmetros ou valores retornados de um contrato de operação. Por exemplo, considere o contrato de operação a seguir.
[OperationContract]
void SubmitOrder(string customerID, string item, int quantity);
<OperationContract()> _
Sub SubmitOrder( _
ByVal customerID As String, _
ByVal item As String, _
ByVal quantity As Integer)
A estrutura de serviço decide automaticamente colocar todas as três informações (customerID
, item
e quantity
) no corpo da mensagem e encapsulá-las em um elemento wrapper chamado SubmitOrderRequest
.
A descrição das informações a serem enviadas ou recebidas como uma lista simples de parâmetros de contrato de operação é a abordagem recomendada, a menos que existam motivos especiais para migrar para o contrato de mensagem mais complexo ou para modelos de programação baseados em Message
.
5. Fluxo
Usar Stream
ou uma de suas subclasses em um contrato de operação ou como uma única parte do corpo da mensagem em um contrato de mensagem pode ser considerado um modelo de programação separado dos descritos acima. O uso de Stream
dessa forma é a única maneira de garantir que seu contrato será utilizável de forma transmitida, além de escrever sua própria subclasse Message
compatível com streaming. Para saber mais, confira Dados grandes e streaming.
Quando Stream
ou uma de suas subclasses é usada dessa forma, o serializador não é invocado. Para mensagens de saída, uma subclasse de streaming Message
especial é criada e o fluxo é gravado conforme descrito na seção na interface do IStreamProvider. Para mensagens de entrada, a estrutura de serviço cria uma subclasse Stream
sobre a mensagem de entrada e a fornece à operação.
Restrições do Modelo de Programação
Os modelos de programação descritos acima não podem ser arbitrariamente combinados. Por exemplo, se uma operação aceitar um tipo de contrato de mensagem, o contrato de mensagem precisará ser seu único parâmetro de entrada. Além disso, a operação deve retornar uma mensagem vazia (tipo de retorno nulo) ou outro contrato de mensagem. Essas restrições de modelo de programação são descritas nos tópicos para cada modelo de programação específico: Usando contratos de mensagem, Usando a classe de mensageme Dados grandes e streaming.
Formatadores de mensagem
Os modelos de programação descritos acima têm suporte com a conexão de componentes chamados formatores de mensagem na estrutura de serviço. Os formatores de mensagem são tipos que implementam a interface IClientMessageFormatter ou IDispatchMessageFormatter, ou ambas, para uso em clientes e em clientes WCF de serviço, respectivamente.
Os formatadores de mensagens normalmente são conectados por comportamentos. Por exemplo, os plugs DataContractSerializerOperationBehavior no formatador de mensagens do contrato de dados. Isso é feito no lado do serviço com a definição de Formatter como o formatador correto no método ApplyDispatchBehavior(OperationDescription, DispatchOperation), ou no lado do cliente com a definição de Formatter como o formatador correto no método ApplyClientBehavior(OperationDescription, ClientOperation).
As tabelas a seguir listam os métodos que um formador de mensagem pode implementar.
Interface | Método | Ação |
---|---|---|
IDispatchMessageFormatter | DeserializeRequest(Message, Object[]) | Converte uma Message de entrada em parâmetros de operação |
IDispatchMessageFormatter | SerializeReply(MessageVersion, Object[], Object) | Cria uma Message de saída de parâmetros de valor retornado/saída da operação |
IClientMessageFormatter | SerializeRequest(MessageVersion, Object[]) | Cria uma Message de saída de parâmetros de operação |
IClientMessageFormatter | DeserializeReply(Message, Object[]) | Converte uma Message de entrada em parâmetros de valor retornado/saída |
Serialização
Sempre que você usa contratos de mensagens ou parâmetros para descrever o conteúdo da mensagem, precisa usar a serialização para converter entre tipos do .NET Framework e representação de Infoset XML. A serialização é usada em outros locais no WCF, por exemplo, Message tem um método GetBody genérico que você pode usar para ler todo o corpo da mensagem desserializada em um objeto.
O WCF dá suporte a duas tecnologias de serialização "prontas para uso" a fim de serializar e desserializar parâmetros e partes de mensagem: a DataContractSerializer e a XmlSerializer
. Além disso, você pode escrever serializadores personalizados. No entanto, outras partes do WCF (como o método genérico GetBody
ou a serialização de falhas SOAP) podem ser restringidas para usar apenas as subclasses XmlObjectSerializer (DataContractSerializer e NetDataContractSerializer, mas não a XmlSerializer), ou podem até ser codificadas para usar apenas o DataContractSerializer.
O XmlSerializer
é o mecanismo de serialização usado em serviços Web ASP.NET. O DataContractSerializer
é o novo mecanismo de serialização que entende o novo modelo de programação do contrato de dados. DataContractSerializer
é a opção padrão, e a opção de usar XmlSerializer
pode ser feita conforme a operação usando o atributo DataContractFormatAttribute.
DataContractSerializerOperationBehavior e XmlSerializerOperationBehavior são os comportamentos de operação responsáveis por conectar os formatadores de mensagem para DataContractSerializer
e XmlSerializer
, respectivamente. O comportamento DataContractSerializerOperationBehavior pode realmente operar com qualquer serializador que derive de XmlObjectSerializer, incluindo NetDataContractSerializer (descrito em detalhes em Usando serialização autônoma). O comportamento chama uma das sobrecargas do método virtual CreateSerializer
para obter o serializador. Para conectar um serializador diferente, crie uma subclasse DataContractSerializerOperationBehavior e substitua ambas as sobrecargas CreateSerializer
.