Dela via


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);
}

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

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

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

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

Describing Empty Messages

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

[OperationContract]

public int GetCurrentTemperature();

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

[OperationContract]
public void SetTemperature(int temperature);

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

[OperationContract(IsOneWay=true)]
public void SetLightbulbStatus(bool isOn);

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

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

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

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

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

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

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

These additional faults can be generated by throwing a FaultException 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);

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

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.

[ServiceContract]
[ServiceBehavior(MaxItemsInObjectGraph=100000)]
public interface IDataService
{
    [OperationContract] DataPoint[] GetData();
}

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

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

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

Tasks

How to: Enable Streaming
How to: Create a Basic Data Contract for a Class or Structure

Concepts

Using the XmlSerializer Class