Επεξεργασία

Κοινή χρήση μέσω


Specifying Data Transfer in Service Contracts

The Windows Communication Foundation (WCF) can be thought of as a messaging infrastructure. Service operations can receive messages, process them, and send them messages. Messages are described using operation contracts. For example, consider the following contract.

[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  

Here, the GetAirfare operation accepts a message with information about fromCity and toCity, and then returns a message that contains a number.

This topic explains the various ways in which an operation contract can describe messages.

Describing Messages by Using Parameters

The simplest way to describe a message is to use a parameter list and the return value. In the preceding example, the fromCity and toCity string parameters were used to describe the request message, and the float return value was used to describe the reply message. If the return value alone is not enough to describe a reply message, out parameters may be used. For example, the following operation has fromCity and toCity in its request message, and a number together with a currency in its reply message:

[OperationContract]  
float GetAirfare(string fromCity, string toCity, out string currency);  
<OperationContract()>  
    Function GetAirfare(fromCity As String, toCity As String) As Double  

Additionally, you may use reference parameters to make a parameter part of both the request and the reply message. The parameters must be of types that can be serialized (converted to XML). By default, WCF uses a component called the DataContractSerializer class to perform this conversion. Most primitive types (such as int, string, float, and DateTime.) are supported. User-defined types must normally have a data contract. For more information, see Using Data Contracts.

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  

Occasionally, the DataContractSerializer is not adequate to serialize your types. WCF supports an alternative serialization engine, the XmlSerializer, which you can also use to serialize parameters. The XmlSerializer allows you to use more control over the resultant XML using attributes such as the XmlAttributeAttribute. To switch to using the XmlSerializer for a particular operation or for the entire service, apply the XmlSerializerFormatAttribute attribute to an operation or a service. For example:

[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  

For more information, see Using the XmlSerializer Class. Remember that manually switching to the XmlSerializer as shown here is not recommended unless you have specific reasons to do so as detailed in that topic.

To isolate .NET parameter names from contract names, you can use the MessageParameterAttribute attribute, and use the Name property to set the contract name. For example, the following operation contract is equivalent to the first example in this topic.

[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  

Describing Empty Messages

An empty request message can be described by having no input or reference parameters. For example, in C#:

[OperationContract]

public int GetCurrentTemperature();

For example, in Visual Basic:

<OperationContract()>

Function GetCurrentTemperature() as Integer

An empty reply message can be described by having a void return type and no output or reference parameters. For example in:

[OperationContract]  
public void SetTemperature(int temperature);  
<OperationContract()>  
Sub SetTemperature(temperature As Integer)  

This is different from a one-way operation, such as:

[OperationContract(IsOneWay=true)]  
public void SetLightbulbStatus(bool isOn);  
<OperationContract(IsOneWay:=True)>  
Sub SetLightbulbStatus(isOne As Boolean)  

The SetTemperatureStatus operation returns an empty message. It may return a fault instead if there is a problem processing the input message. The SetLightbulbStatus operation returns nothing. There is no way to communicate a fault condition from this operation.

Describing Messages by Using Message Contracts

You may want to use a single type to represent the entire message. While it is possible to use a data contract for this purpose, the recommended way to do this is to use a message contract—this avoids unnecessary levels of wrapping in the resultant XML. Additionally, message contracts allow you to exercise more control over resultant messages. For instance, you can decide which pieces of information should be in the message body and which should be in the message headers. The following example shows the use of message contracts.

[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  

For more information, see Using Message Contracts.

In the previous example, the DataContractSerializer class is still used by default. The XmlSerializer class can also be used with message contracts. To do this, apply the XmlSerializerFormatAttribute attribute to either the operation or the contract, and use types compatible with the XmlSerializer class in the message headers and body members.

Describing Messages by Using Streams

Another way to describe messages in operations is to use the Stream class or one of its derived classes in an operation contract or as a message contract body member (it must be the only member in this case). For incoming messages, the type must be Stream—you cannot use derived classes.

Instead of invoking the serializer, WCF retrieves data from a stream and puts it directly into an outgoing message, or retrieves data from an incoming message and puts it directly into a stream. The following sample shows the use of streams.

[OperationContract]  
public Stream DownloadFile(string fileName);  
<OperationContract()>  
Function DownloadFile(fileName As String) As String  

You cannot combine Stream and non-stream data in a single message body. Use a message contract to put the extra data in message headers. The following example shows the incorrect usage of streams when defining the operation contract.

//Incorrect:  
// [OperationContract]  
// public void UploadFile (string fileName, Stream fileData);  
'Incorrect:  
    '<OperationContract()>  
    Public Sub UploadFile(fileName As String, fileData As StreamingContext)  

The following sample shows the correct usage of streams when defining an operation contract.

[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  

For more information, see Large Data and Streaming.

Using the Message Class

To have complete programmatic control over messages sent or received, you can use the Message class directly, as shown in the following example code.

[OperationContract]  
public void LogMessage(Message m);  
<OperationContract()>  
Sub LogMessage(m As Message)  

This is an advanced scenario, which is described in detail in Using the Message Class.

Describing Fault Messages

In addition to the messages that are described by the return value and output or reference parameters, any operation that is not one-way can return at least two possible messages: its normal response message and a fault message. Consider the following operation contract.

[OperationContract]  
float GetAirfare(string fromCity, string toCity, DateTime date);  
<OperationContract()>  
Function GetAirfare(fromCity As String, toCity As String, date as DateTime)  

This operation may either return a normal message that contains a float number, or a fault message that contains a fault code and a description. You can accomplish this by throwing a FaultException in your service implementation.

You can specify additional possible fault messages by using the FaultContractAttribute attribute. The additional faults must be serializable using the DataContractSerializer, as shown in the following example code.

[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  

These additional faults can be generated by throwing a FaultException<TDetail> of the appropriate data contract type. For more information, see Handling Exceptions and Faults.

You cannot use the XmlSerializer class to describe faults. The XmlSerializerFormatAttribute has no effect on fault contracts.

Using Derived Types

You may want to use a base type in an operation or a message contract, and then use a derived type when actually invoking the operation. In this case, you must use either the ServiceKnownTypeAttribute attribute or some alternative mechanism to allow the use of derived types. Consider the following operation.

[OperationContract]  
public bool IsLibraryItemAvailable(LibraryItem item);  
<OperationContract()>  
    Function IsLibraryItemAvailable(item As LibraryItem) As Boolean  

Assume that two types, Book and Magazine, derive from LibraryItem. To use these types in the IsLibraryItemAvailable operation, you can change the operation as follows:

[OperationContract]

[ServiceKnownType(typeof(Book))]

[ServiceKnownType(typeof(Magazine))]

public bool IsLibraryItemAvailable(LibraryItem item);

Alternatively, you can use the KnownTypeAttribute attribute when the default DataContractSerializer is in use, as shown in the following example code.

[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  

You can use the XmlIncludeAttribute attribute when using the XmlSerializer.

You can apply the ServiceKnownTypeAttribute attribute to an operation or to the entire service. It accepts either a type or the name of the method to call to get a list of known types, just like the KnownTypeAttribute attribute. For more information, see Data Contract Known Types.

Specifying the Use and Style

When describing services using Web Services Description Language (WSDL), the two commonly used styles are Document and remote procedure call (RPC). In the Document style, the entire message body is described using the schema, and the WSDL describes the various message body parts by referring to elements within that schema. In the RPC style, the WSDL refers to a schema type for each message part rather than an element. In some cases, you have to manually select one of these styles. You can do this by applying the DataContractFormatAttribute attribute and setting the Style property (when the DataContractSerializer is in use), or by setting Style on the XmlSerializerFormatAttribute attribute (when using the XmlSerializer).

Additionally, the XmlSerializer supports two forms of serialized XML: Literal and Encoded. Literal is the most commonly accepted form, and is the only form the DataContractSerializer supports. Encoded is a legacy form described in section 5 of the SOAP specification, and is not recommended for new services. To switch to Encoded mode, set the Use property on the XmlSerializerFormatAttribute attribute to Encoded.

In most cases, you should not change the default settings for the Style and Use properties.

Controlling the Serialization Process

You can do a number of things to customize the way data is serialized.

Changing Server Serialization Settings

When the default DataContractSerializer is in use, you can control some aspects of the serialization process on the service by applying the ServiceBehaviorAttribute attribute to the service. Specifically, you may use the MaxItemsInObjectGraph property to set the quota that limits the maximum number of objects the DataContractSerializer deserializes. You can use the IgnoreExtensionDataObject property to turn off the round-tripping versioning feature. For more information about quotas, see Security Considerations for Data. For more information about round-tripping, see Forward-Compatible Data Contracts.

[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  

Serialization Behaviors

Two behaviors are available in WCF, the DataContractSerializerOperationBehavior and the XmlSerializerOperationBehavior that are automatically plugged in depending on which serializer is in use for a particular operation. Because these behaviors are applied automatically, you normally do not have to be aware of them.

However, the DataContractSerializerOperationBehavior has the MaxItemsInObjectGraph, IgnoreExtensionDataObject, and DataContractSurrogate properties that you may use to customize the serialization process. The first two properties have the same meaning as discussed in the previous section. You can use the DataContractSurrogate property to enable data contract surrogates, which are a powerful mechanism for customizing and extending the serialization process. For more information, see Data Contract Surrogates.

You can use the DataContractSerializerOperationBehavior to customize both client and server serialization. The following example shows how to increase the MaxItemsInObjectGraph quota on the client.

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  

The following is the equivalent code on the service, in the self-hosted case:

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()  

In the Web-hosted case, you must create a new ServiceHost derived class and use a service host factory to plug it in.

Controlling Serialization Settings in Configuration

The MaxItemsInObjectGraph and IgnoreExtensionDataObject can be controlled through configuration by using the dataContractSerializer endpoint or service behavior, as shown in the following example.

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

Shared Type Serialization, Object Graph Preservation, and Custom Serializers

The DataContractSerializer serializes using data contract names and not .NET type names. This is consistent with service-oriented architecture tenets and allows for a great degree of flexibility—the .NET types can change without affecting the wire contract. In rare cases, you may want to serialize actual .NET type names, thereby introducing a tight coupling between the client and the server, similar to the .NET Framework remoting technology. This is not a recommended practice, except in rare cases that usually occur when migrating to WCF from .NET Framework remoting. In this case, you must use the NetDataContractSerializer class instead of the DataContractSerializer class.

The DataContractSerializer normally serializes object graphs as object trees. That is, if the same object is referred to more than once, it is serialized more than once. For example, consider a PurchaseOrder instance that has two fields of type Address called billTo and shipTo. If both fields are set to the same Address instance, there are two identical Address instances after serialization and deserialization. This is done because there is no standard interoperable way to represent object graphs in XML (except for the legacy SOAP encoded standard available on the XmlSerializer, as described in the previous section on Style and Use). Serializing object graphs as trees has certain disadvantages, for example, graphs with circular references cannot be serialized. Occasionally, it is necessary to switch to true object graph serialization, even though it is not interoperable. This can be done by using the DataContractSerializer constructed with the preserveObjectReferences parameter set to true.

Occasionally, the built-in serializers are not enough for your scenario. In most cases, you can still use the XmlObjectSerializer abstraction from which both the DataContractSerializer and the NetDataContractSerializer derive.

The previous three cases (.NET type preservation, object graph preservation, and completely custom XmlObjectSerializer-based serialization) all require a custom serializer be plugged in. To do this, perform the following steps:

  1. Write your own behavior deriving from the DataContractSerializerOperationBehavior.

  2. Override the two CreateSerializer methods to return your own serializer (either the NetDataContractSerializer, the DataContractSerializer with preserveObjectReferences set to true, or your own custom XmlObjectSerializer).

  3. Before opening the service host or creating a client channel, remove the existing DataContractSerializerOperationBehavior behavior and plug in the custom derived class that you created in the previous steps.

For more information about advanced serialization concepts, see Serialization and Deserialization.

See also