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:

  1. Escreva seu próprio comportamento por meio da derivação de DataContractSerializerOperationBehavior.

  2. Substitua os dois métodos CreateSerializer para retornar seu próprio serializador (seja o NetDataContractSerializer, o DataContractSerializer com preserveObjectReferences definido como true ou seu próprio XmlObjectSerializer personalizado).

  3. 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.

Confira também