Especificando transferência de dados em contratos de serviço
O WCF (Windows Communication Foundation) pode ser pensado como uma infraestrutura de mensagens. As operações de serviço podem receber e processar mensagens para, depois, responder com outras mensagens. As mensagens são descritas usando contratos de operação. Por exemplo, considere o contrato a seguir.
[ServiceContract]
public interface IAirfareQuoteService
{
[OperationContract]
float GetAirfare(string fromCity, string toCity);
}
<ServiceContract()>
Public Interface IAirfareQuoteService
<OperationContract()>
Function GetAirfare(fromCity As String, toCity As String) As Double
End Interface
Aqui, a operação GetAirfare
aceita uma mensagem com informações sobre fromCity
e toCity
e, em seguida, retorna uma mensagem que contém um número.
Este tópico explica as diversas formas que um contrato de operação pode descrever mensagens.
Descrever mensagens usando parâmetros
A maneira mais simples de descrever uma mensagem é usar uma lista de parâmetros e o valor retornado. No exemplo anterior, os parâmetros de cadeia de caracteres fromCity
e toCity
foram usados para descrever a mensagem de solicitação e o valor de retorno flutuante foi usado para descrever a mensagem de resposta. Se o valor de retorno por si só não for suficiente para descrever uma mensagem de resposta, os parâmetros de saída podem ser usados. Por exemplo, a operação a seguir tem fromCity
e toCity
na mensagem de solicitação e um número com uma moeda na mensagem de resposta:
[OperationContract]
float GetAirfare(string fromCity, string toCity, out string currency);
<OperationContract()>
Function GetAirfare(fromCity As String, toCity As String) As Double
Além disso, é possível usar parâmetros de referência para tornar um parâmetro parte da solicitação e da mensagem de resposta. Os parâmetros devem ser de tipos que podem ser serializados (convertidos em XML). Por padrão, o WCF usa um componente chamado de classe DataContractSerializer para executar essa conversão. Há suporte para a maioria dos tipos primitivos (como int
, string
, float
e DateTime
). Os tipos definidos pelo usuário normalmente devem ter um contrato de dados. Para saber mais, confira Como usar contratos de dados.
public interface IAirfareQuoteService
{
[OperationContract]
float GetAirfare(Itinerary itinerary, DateTime date);
[DataContract]
public class Itinerary
{
[DataMember]
public string fromCity;
[DataMember]
public string toCity;
}
}
Public Interface IAirfareQuoteService
<OperationContract()>
GetAirfare(itinerary as Itinerary, date as DateTime) as Double
<DataContract()>
Class Itinerary
<DataMember()>
Public fromCity As String
<DataMember()>
Public toCity As String
End Class
End Interface
Às vezes, o DataContractSerializer
não é adequado para serializar seus tipos. O WCF dá suporte a um mecanismo de serialização alternativo, o XmlSerializer, que também pode ser usado para serializar parâmetros. O XmlSerializer permite mais controle sobre o XML resultante por meio de atributos como o XmlAttributeAttribute
. Se você quiser alternar para o uso do XmlSerializer em uma operação específica ou em todo o serviço, aplique o atributo XmlSerializerFormatAttribute à operação ou ao serviço. Por exemplo:
[ServiceContract]
public interface IAirfareQuoteService
{
[OperationContract]
[XmlSerializerFormat]
float GetAirfare(Itinerary itinerary, DateTime date);
}
public class Itinerary
{
public string fromCity;
public string toCity;
[XmlAttribute]
public bool isFirstClass;
}
<ServiceContract()>
Public Interface IAirfareQuoteService
<OperationContract()>
<XmlSerializerFormat>
GetAirfare(itinerary as Itinerary, date as DateTime) as Double
End Interface
Class Itinerary
Public fromCity As String
Public toCity As String
<XmlSerializerFormat()>
Public isFirstClass As Boolean
End Class
Para saber mais, confira Uso da classe XmlSerializer. Lembre-se de que alternar manualmente para o XmlSerializer, conforme mostrado aqui, não é recomendado, a menos que você tenha motivos específicos para isso, conforme detalhado nesse tópico.
Para isolar nomes de parâmetro .NET de nomes de contrato, é possível usar o atributo MessageParameterAttribute e a propriedade Name
para definir o nome do contrato. Por exemplo, o contrato de operação a seguir é equivalente ao primeiro exemplo neste tópico.
[OperationContract]
public float GetAirfare(
[MessageParameter(Name="fromCity")] string originCity,
[MessageParameter(Name="toCity")] string destinationCity);
<OperationContract()>
Function GetAirfare(<MessageParameter(Name := "fromCity")> fromCity As String, <MessageParameter(Name := "toCity")> toCity As String) As Double
Descrever mensagens vazias
Uma mensagem de solicitação vazia pode ser descrita como uma mensagem que não tem parâmetros de entrada ou referência. Por exemplo, em C#:
[OperationContract]
public int GetCurrentTemperature();
Por exemplo, no Visual Basic:
<OperationContract()>
Function GetCurrentTemperature() as Integer
Uma mensagem de resposta vazia pode ser descrita como uma mensagem que tem um tipo de retorno void
e nenhum parâmetro de saída ou referência. Por exemplo, no seguinte:
[OperationContract]
public void SetTemperature(int temperature);
<OperationContract()>
Sub SetTemperature(temperature As Integer)
Isso é diferente de uma operação unidirecional, como a seguinte:
[OperationContract(IsOneWay=true)]
public void SetLightbulbStatus(bool isOn);
<OperationContract(IsOneWay:=True)>
Sub SetLightbulbStatus(isOne As Boolean)
A operação SetTemperatureStatus
retorna uma mensagem vazia. Ela poderá retornar uma falha se houver um problema ao processar a mensagem de entrada. A operação SetLightbulbStatus
não retorna nada. Não há como comunicar uma condição de falha desta operação.
Descrever mensagens usando contratos de mensagem
É possível usar um único tipo para representar toda a mensagem. Embora seja possível usar um contrato de dados para essa finalidade, a maneira recomendada de fazer isso é usar um contrato de mensagem, que evita níveis desnecessários de encapsulamento no XML resultante. Além disso, os contratos de mensagem permitem exercer mais controle sobre as mensagens resultantes. Por exemplo, é possível decidir quais informações devem estar no corpo da mensagem e quais devem estar nos cabeçalhos dela. O exemplo a seguir mostra o uso de contratos de mensagem.
[ServiceContract]
public interface IAirfareQuoteService
{
[OperationContract]
GetAirfareResponse GetAirfare(GetAirfareRequest request);
}
[MessageContract]
public class GetAirfareRequest
{
[MessageHeader] public DateTime date;
[MessageBodyMember] public Itinerary itinerary;
}
[MessageContract]
public class GetAirfareResponse
{
[MessageBodyMember] public float airfare;
[MessageBodyMember] public string currency;
}
[DataContract]
public class Itinerary
{
[DataMember] public string fromCity;
[DataMember] public string toCity;
}
<ServiceContract()>
Public Interface IAirfareQuoteService
<OperationContract()>
Function GetAirfare(request As GetAirfareRequest) As GetAirfareResponse
End Interface
<MessageContract()>
Public Class GetAirfareRequest
<MessageHeader()>
Public Property date as DateTime
<MessageBodyMember()>
Public Property itinerary As Itinerary
End Class
<MessageContract()>
Public Class GetAirfareResponse
<MessageBodyMember()>
Public Property airfare As Double
<MessageBodyMember()> Public Property currency As String
End Class
<DataContract()>
Public Class Itinerary
<DataMember()> Public Property fromCity As String
<DataMember()> Public Property toCity As String
End Class
Para saber mais, confira Como usar contratos de mensagem.
No exemplo anterior, a classe DataContractSerializer ainda é usada por padrão. A classe XmlSerializer também pode ser usada com contratos de mensagem. Para fazer isso, aplique o atributo XmlSerializerFormatAttribute à operação ou ao contrato e use tipos compatíveis com a classe XmlSerializer nos cabeçalhos e nos membros do corpo da mensagem.
Descrever mensagens usando fluxos
Outra maneira de descrever mensagens em operações é usar a classe Stream ou uma de suas classes derivadas em um contrato de operação ou como um membro do corpo do contrato de mensagem (neste caso, deve ser o único membro). Para as mensagens recebidas, o tipo deve ser Stream
e não é possível usar classes derivadas.
Em vez de invocar o serializador, o WCF recupera dados de um fluxo e os coloca diretamente em uma mensagem de saída ou recupera dados de uma mensagem de entrada e os coloca diretamente em um fluxo. O exemplo a seguir mostra o uso de fluxos.
[OperationContract]
public Stream DownloadFile(string fileName);
<OperationContract()>
Function DownloadFile(fileName As String) As String
Não é possível combinar dados que não são de fluxo e Stream
em um único corpo de mensagem. Use um contrato de mensagem para colocar os dados extras nos cabeçalhos das mensagens. O exemplo a seguir mostra o uso incorreto de fluxos ao definir o contrato de operação.
//Incorrect:
// [OperationContract]
// public void UploadFile (string fileName, Stream fileData);
'Incorrect:
'<OperationContract()>
Public Sub UploadFile(fileName As String, fileData As StreamingContext)
O exemplo a seguir mostra o uso correto de fluxos ao definir um contrato de operação.
[OperationContract]
public void UploadFile (UploadFileMessage message);
//code omitted
[MessageContract]
public class UploadFileMessage
{
[MessageHeader] public string fileName;
[MessageBodyMember] public Stream fileData;
}
<OperationContract()>
Public Sub UploadFile(fileName As String, fileData As StreamingContext)
'Code Omitted
<MessageContract()>
Public Class UploadFileMessage
<MessageHeader()>
Public Property fileName As String
<MessageBodyMember()>
Public Property fileData As Stream
End Class
Para saber mais, confira Dados grandes e streaming.
Usando a classe de mensagens
Para ter controle programático completo sobre as mensagens enviadas ou recebidas, é possível usar a classe Message diretamente, conforme mostrado no código de exemplo a seguir.
[OperationContract]
public void LogMessage(Message m);
<OperationContract()>
Sub LogMessage(m As Message)
Este é um cenário avançado, descrito em detalhes em Uso da classe de mensagem.
Descrever mensagens de falha
Além das mensagens descritas pelo valor de retorno e pelos parâmetros de saída ou referência, qualquer operação que não seja unidirecional pode retornar pelo menos duas mensagens possíveis: sua mensagem de resposta normal e uma mensagem de falha. Considere o contrato de operação a seguir.
[OperationContract]
float GetAirfare(string fromCity, string toCity, DateTime date);
<OperationContract()>
Function GetAirfare(fromCity As String, toCity As String, date as DateTime)
Esta operação pode retornar uma mensagem normal que contém um número float
ou uma mensagem de falha que contém um código de falha e uma descrição. Para isso, lance um FaultException na implementação do serviço.
É possível especificar possíveis mensagens de falha adicionais usando o atributo FaultContractAttribute. As falhas adicionais devem ser serializáveis usando o DataContractSerializer, conforme mostrado no código de exemplo a seguir.
[OperationContract]
[FaultContract(typeof(ItineraryNotAvailableFault))]
float GetAirfare(string fromCity, string toCity, DateTime date);
//code omitted
[DataContract]
public class ItineraryNotAvailableFault
{
[DataMember]
public bool IsAlternativeDateAvailable;
[DataMember]
public DateTime alternativeSuggestedDate;
}
<OperationContract()>
<FaultContract(GetType(ItineraryNotAvailableFault))>
Function GetAirfare(fromCity As String, toCity As String, date as DateTime) As Double
'Code Omitted
<DataContract()>
Public Class
<DataMember()>
Public Property IsAlternativeDateAvailable As Boolean
<DataMember()>
Public Property alternativeSuggestedDate As DateTime
End Class
Essas falhas adicionais podem ser geradas lançando um FaultException<TDetail> do tipo de contrato de dados apropriado. Para saber mais, confira Lidar com exceções e falhas.
Não é possível usar a classe XmlSerializer para descrever falhas. O XmlSerializerFormatAttribute não tem efeito nos contratos de falha.
Uso de tipos derivados
É possível usar um tipo base em uma operação ou um contrato de mensagem e, em seguida, usar um tipo derivado ao realmente chamar a operação. Nesse caso, é necessário usar o atributo ServiceKnownTypeAttribute ou algum mecanismo alternativo para permitir o uso de tipos derivados. Considere a operação a seguir.
[OperationContract]
public bool IsLibraryItemAvailable(LibraryItem item);
<OperationContract()>
Function IsLibraryItemAvailable(item As LibraryItem) As Boolean
Suponha que dois tipos, Book
e Magazine
, derivam de LibraryItem
. Para usar esses tipos na operação IsLibraryItemAvailable
, é possível alterá-la da seguinte maneira:
[OperationContract]
[ServiceKnownType(typeof(Book))]
[ServiceKnownType(typeof(Magazine))]
public bool IsLibraryItemAvailable(LibraryItem item);
Como alternativa, é possível usar o atributo KnownTypeAttribute quando o DataContractSerializer padrão estiver em uso, conforme mostrado no código de exemplo a seguir.
[OperationContract]
public bool IsLibraryItemAvailable(LibraryItem item);
// code omitted
[DataContract]
[KnownType(typeof(Book))]
[KnownType(typeof(Magazine))]
public class LibraryItem
{
//code omitted
}
<OperationContract()>
Function IsLibraryItemAvailable(item As LibraryItem) As Boolean
'Code Omitted
<DataContract()>
<KnownType(GetType(Book))>
<KnownType(GetType(Magazine))>
Public Class LibraryItem
'Code Omitted
End Class
É possível usar o atributo XmlIncludeAttribute ao usar o XmlSerializer.
É possível aplicar o atributo ServiceKnownTypeAttribute a uma operação ou a todo o serviço. Ele aceita um tipo ou o nome do método a ser chamado para obter uma lista de tipos conhecidos, assim como o atributo KnownTypeAttribute. Para saber mais, confira Tipos de contrato de dados conhecidos.
Especificação do uso e do estilo
Ao descrever serviços usando WSDL (Web Services Description Language), os dois estilos comumente usados são Documento e RPC (chamada de procedimento remoto). No estilo Documento, todo o corpo da mensagem é descrito usando o esquema e o WSDL descreve as diversas partes do corpo da mensagem referindo-se aos elementos desse esquema. No estilo RPC, o WSDL refere-se a um tipo de esquema para cada parte da mensagem, em vez de a um elemento. Em alguns casos, é necessário selecionar manualmente um desses estilos. É possível fazer isso aplicando o atributo DataContractFormatAttribute e definindo a propriedade Style
(quando DataContractSerializer estiver em uso) ou definindo Style
no atributo XmlSerializerFormatAttribute (ao usar XmlSerializer).
Além disso, o XmlSerializer dá suporte a duas formas de XML serializado: Literal
e Encoded
. Literal
é a forma mais comumente aceita e é a única com suporte no DataContractSerializer. Encoded
é uma forma herdada descrita na seção 5 da especificação SOAP que não é recomendada para novos serviços. Para alternar o modo Encoded
, configure a propriedade Use
no atributo XmlSerializerFormatAttribute para Encoded
.
Na maioria dos casos, não é necessário alterar as configurações padrão para as propriedades Style
e Use
.
Controle do processo de serialização
Há muitas formas de personalizar o processo de serialização dos dados.
Alterar as configurações de serialização do servidor
Quando o DataContractSerializer padrão está em uso, é possível controlar alguns aspectos do processo de serialização no serviço aplicando o atributo ServiceBehaviorAttribute ao serviço. Especificamente, é possível usar a propriedade MaxItemsInObjectGraph
para definir a cota que limita o número máximo de objetos que o DataContractSerializer desserializa. É possível usar a propriedade IgnoreExtensionDataObject
para desativar o recurso de versão de ida e volta. Para obter mais informações sobre cotas, confira Considerações sobre segurança de dados. Para saber mais sobre a viagem de ida e volta, confira Contratos de dados compatíveis com versões posteriores.
[ServiceBehavior(MaxItemsInObjectGraph=100000)]
public class MyDataService:IDataService
{
public DataPoint[] GetData()
{
// Implementation omitted
}
}
<ServiceBehavior(MaxItemsInObjectGraph:=100000)>
Public Class MyDataService Implements IDataService
Function GetData() As DataPoint()
‘ Implementation omitted
End Function
End Interface
Comportamentos de serialização
Estão disponíveis no WCF dois comportamentos (DataContractSerializerOperationBehavior e XmlSerializerOperationBehavior) que são conectados automaticamente dependendo de qual serializador está em uso para uma operação específica. Como esses comportamentos são aplicados automaticamente, normalmente não é necessário estar ciente deles.
No entanto, o DataContractSerializerOperationBehavior
tem as propriedades MaxItemsInObjectGraph
, IgnoreExtensionDataObject
e DataContractSurrogate
que podem ser usadas para personalizar o processo de serialização. As duas primeiras propriedades têm o mesmo significado discutido na seção anterior. É possível usar a propriedade DataContractSurrogate
para habilitar substitutos de contrato de dados, que são um mecanismo avançado para personalizar e estender o processo de serialização. Para saber mais, confira Substitutos do contrato de dados.
É possível usar DataContractSerializerOperationBehavior
para personalizar a serialização do cliente e do servidor. O exemplo a seguir mostra como aumentar a cota MaxItemsInObjectGraph
no cliente.
ChannelFactory<IDataService> factory = new ChannelFactory<IDataService>(binding, address);
foreach (OperationDescription op in factory.Endpoint.Contract.Operations)
{
DataContractSerializerOperationBehavior dataContractBehavior =
op.Behaviors.Find<DataContractSerializerOperationBehavior>()
as DataContractSerializerOperationBehavior;
if (dataContractBehavior != null)
{
dataContractBehavior.MaxItemsInObjectGraph = 100000;
}
}
IDataService client = factory.CreateChannel();
Dim factory As ChannelFactory(Of IDataService) = New ChannelFactory(Of IDataService)(binding, address)
For Each op As OperationDescription In factory.Endpoint.Contract.Operations
Dim dataContractBehavior As DataContractSerializerOperationBehavior = op.Behaviors.Find(Of DataContractSerializerOperationBehavior)()
If dataContractBehavior IsNot Nothing Then
dataContractBehavior.MaxItemsInObjectGraph = 100000
End If
Next
Dim client As IDataService = factory.CreateChannel
Veja o seguinte código equivalente no serviço, no caso auto-hospedado:
ServiceHost serviceHost = new ServiceHost(typeof(IDataService))
foreach (ServiceEndpoint ep in serviceHost.Description.Endpoints)
{
foreach (OperationDescription op in ep.Contract.Operations)
{
DataContractSerializerOperationBehavior dataContractBehavior =
op.Behaviors.Find<DataContractSerializerOperationBehavior>()
as DataContractSerializerOperationBehavior;
if (dataContractBehavior != null)
{
dataContractBehavior.MaxItemsInObjectGraph = 100000;
}
}
}
serviceHost.Open();
Dim serviceHost As ServiceHost = New ServiceHost(GetType(IDataService))
For Each ep As ServiceEndpoint In serviceHost.Description.Endpoints
For Each op As OperationDescription In ep.Contract.Operations
Dim dataContractBehavior As DataContractSerializerOperationBehavior = op.Behaviors.Find(Of DataContractSerializerOperationBehavior)()
If dataContractBehavior IsNot Nothing Then
dataContractBehavior.MaxItemsInObjectGraph = 100000
End If
Next
Next
serviceHost.Open()
No caso da hospedagem na Web, é necessário criar uma classe derivada ServiceHost
e usar um factory de host de serviço para conectá-la.
Controle das configurações de serialização na configuração
É possível controlar MaxItemsInObjectGraph
e IgnoreExtensionDataObject
por meio da configuração usando o ponto de extremidade dataContractSerializer
ou o comportamento do serviço, conforme mostrado no exemplo a seguir.
<configuration>
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="LargeQuotaBehavior">
<dataContractSerializer
maxItemsInObjectGraph="100000" />
</behavior>
</endpointBehaviors>
</behaviors>
<client>
<endpoint address="http://example.com/myservice"
behaviorConfiguration="LargeQuotaBehavior"
binding="basicHttpBinding" bindingConfiguration=""
contract="IDataService"
name="" />
</client>
</system.serviceModel>
</configuration>
Serialização de tipo compartilhado, preservação do grafo de objetos e serializadores personalizados
O DataContractSerializer faz a serialização usando nomes de contrato de dados e não nomes de tipo .NET. Isso é consistente com os princípios da arquitetura orientada a serviços e permite um grande grau de flexibilidade, visto que os tipos .NET podem mudar sem afetar o contrato de transmissão. Em casos raros, é possível serializar nomes de tipo .NET reais, introduzindo um acoplamento estreito entre o cliente e o servidor, semelhante à tecnologia remota do .NET Framework. Essa não é uma prática recomendada, exceto em casos raros que geralmente ocorrem ao migrar para o WCF do .NET Framework remoto. Nesse caso, é necessário usar a classe NetDataContractSerializer em vez da classe DataContractSerializer.
O DataContractSerializer normalmente serializa grafos de objetos como árvores de objetos. Isso significa que, se o mesmo objeto for referido mais de uma vez, ele será serializado mais de uma vez. Por exemplo, considere uma instância PurchaseOrder
que tem dois campos do tipo Endereço chamados billTo
e shipTo
. Se ambos os campos forem definidos para a mesma instância de Endereço, haverá duas instâncias idênticas após a serialização e a desserialização. Isso ocorre porque não há uma maneira interoperável padrão de representar grafos de objetos em XML (exceto com relação ao padrão codificado SOAP herdado disponível no XmlSerializer, conforme descrito na seção anterior sobre Style
e Use
). A serialização de grafos de objetos como árvores tem certas desvantagens, por exemplo, não é possível serializar grafos com referências circulares. Ocasionalmente, é necessário alternar para a verdadeira serialização de grafo de objetos, mesmo que não haja interoperabilidade. Isso pode ser feito usando o DataContractSerializer criado com o parâmetro preserveObjectReferences
definido como true
.
Ocasionalmente, os serializadores internos não são suficientes para o seu cenário. Na maioria dos casos, ainda é possível usar a abstração XmlObjectSerializer da qual tanto DataContractSerializer quanto NetDataContractSerializer são derivados.
Os três casos anteriores (preservação de tipo .NET, preservação de grafo de objetos e serialização completamente personalizada e baseada em XmlObjectSerializer
) exigem que um serializador personalizado seja conectado. Para fazer isso, execute estas etapas:
Escreva seu próprio comportamento por meio da derivação de DataContractSerializerOperationBehavior.
Substitua os dois métodos
CreateSerializer
para retornar seu próprio serializador (seja o NetDataContractSerializer, o DataContractSerializer compreserveObjectReferences
definido comotrue
ou seu próprio XmlObjectSerializer personalizado).Antes de abrir o host de serviço ou criar um canal cliente, remova o comportamento DataContractSerializerOperationBehavior existente e conecte a classe derivada personalizada criada nas etapas anteriores.
Para saber mais sobre conceitos avançados de serialização, confira Serialização e desserialização.