在服务协定中指定数据传输

Windows Communication Foundation (WCF)可以视为消息传递基础结构。 服务操作可以接收消息、处理消息并发送消息。 消息是使用操作协定描述的。 例如,请考虑以下协定。

[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  

在此,GetAirfare 操作接受包含fromCitytoCity信息的消息,然后返回一个包含数字的消息。

本主题解释了操作合同可以用来描述消息的各种方式。

使用参数描述消息

描述消息的最简单方法是使用参数列表和返回值。 在前面的示例中,fromCitytoCity使用了字符串参数来描述请求消息,而浮点返回值用于描述回复消息。 如果仅返回值不足以描述回复消息,可以使用 out 参数。 例如,以下操作在其请求消息中含有 fromCitytoCity,而其答复消息中包含一个数字和货币:

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

此外,您可以使用引用参数使参数成为请求和答复消息的一部分。 参数必须是可以序列化的类型(转换为 XML)。 默认情况下,WCF 使用名为类的 DataContractSerializer 组件来执行此转换。 支持大多数基元类型(例如intstringfloatDateTime.)。 用户定义的类型通常必须具有数据协定。 有关详细信息,请参阅 使用数据协定

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  

有时,DataContractSerializer 无法充分序列化您的类型。 WCF 支持替代序列化引擎, XmlSerializer该引擎还可用于序列化参数。 通过使用XmlSerializer,您可以使用诸如XmlAttributeAttribute之类的属性来对生成的XML进行更多控制。 若要切换到对特定作或整个服务使用 XmlSerializer ,请将 XmlSerializerFormatAttribute 属性应用于作或服务。 例如:

[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  

有关详细信息,请参阅 使用 XmlSerializer 类。 请记住,最好不要如此处所示手动切换到 XmlSerializer,除非有该主题详细讨论的特定原因需要执行此操作。

若要将 .NET 参数名称与协定名称隔离开来,可以使用 MessageParameterAttribute 属性来实现此目的,并可以使用 Name 属性来设置协定名称。 例如,以下作协定等效于本主题中的第一个示例。

[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  

描述空消息

如果没有输入或引用参数,可以描述空请求消息。 例如,在 C# 中:

[OperationContract]

public int GetCurrentTemperature();

例如,在 Visual Basic 中:

<OperationContract()>

Function GetCurrentTemperature() as Integer

可以通过具有 void 返回类型且没有输出或引用参数来描述空回复消息。 例如,在:

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

这不同于单向操作,例如:

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

SetTemperatureStatus操作返回一条空消息。 如果处理输入消息时出现问题,可能会返回错误。 该 SetLightbulbStatus 操作不返回任何值。 无法在此操作中传递错误条件。

使用消息协定来描述消息

你可能想要使用单个类型来表示整个消息。 尽管可以为此使用数据协定,但推荐的方式是使用消息协定,这样可以避免在生成的 XML 中出现不必要的包装层次。 此外,消息协定允许你更好地控制生成的消息。 例如,你可以确定哪些信息应位于消息正文中,哪些信息应位于消息标头中。 以下示例显示了消息协定的使用。

[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  

有关详细信息,请参阅 使用消息协定

在前面的示例中, DataContractSerializer 该类默认仍使用。 该 XmlSerializer 类还可用于消息协定。 为此,请将 XmlSerializerFormatAttribute 属性应用于操作或合约,并在消息标头和正文成员中使用与 XmlSerializer 类兼容的类型。

使用流描述消息

在操作中描述消息的另一种方法是,在操作协定中使用Stream类或其派生类之一,或者作为消息协定正文中的唯一成员。 对于传入消息,类型必须为 Stream- 不能使用派生类。

WCF 不调用序列化程序,而是从流中检索数据,并将其直接放入传出消息中,或者从传入消息中检索数据,并将其直接放入流中。 下面的示例演示了流的使用。

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

不能在单个消息正文中合并 Stream 和非流数据。 使用消息协定将额外数据放入消息标头中。 在定义操作合同时,以下示例显示了流的不正确用法。

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

以下示例显示了定义操作契约时正确使用流的方式。

[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  

有关详细信息,请参阅 大型数据和流式处理

使用 Message 类

若要完全以编程方式控制发送或接收的消息,可以直接使用该 Message 类,如以下示例代码所示。

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

这是一种高级方案,在 “使用消息类”中对此进行了详细介绍。

描述错误消息

除了返回值和输出或引用参数描述的消息外,任何非单向作都至少可以返回两条可能的消息:其正常响应消息和错误消息。 请考虑下面的操作协定。

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

此作可能返回包含 float 数字的普通消息,或者返回包含错误代码和说明的错误消息。 可以通过在服务实现中抛出 FaultException 来实现此目的。

可以使用特性 FaultContractAttribute 指定其他可能的错误消息。 附加故障必须使用 DataContractSerializer 进行序列化,如以下示例代码所示。

[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  

可以通过引发相应数据协定类型的 FaultException<TDetail> 来生成这些其他错误。 有关详细信息,请参阅 处理异常和错误

不能使用该 XmlSerializer 类来描述错误。 XmlSerializerFormatAttribute 对错误协定无效。

使用派生类型

你可能希望在操作或消息合同中使用基类型,然后在实际调用操作时使用派生类型。 在这种情况下,必须使用 ServiceKnownTypeAttribute 属性或某种替代机制来允许使用派生类型。 请考虑以下操作。

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

假设有两种类型, Book 并且 Magazine派生自 LibraryItem。 若要在 IsLibraryItemAvailable 操作中使用这些类型,可以按如下所示更改操作:

[OperationContract]

[ServiceKnownType(typeof(Book))]

[ServiceKnownType(typeof(Magazine))]

public bool IsLibraryItemAvailable(LibraryItem item);

或者,当默认值KnownTypeAttribute正在使用时,可以使用DataContractSerializer该属性,如以下示例代码所示。

[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  

在使用XmlIncludeAttribute时,可以使用XmlSerializer属性。

可以将ServiceKnownTypeAttribute属性应用于操作或整个服务。 它接受类型或方法名称,以调用获得已知类型的列表,就像 KnownTypeAttribute 属性一样。 有关详细信息,请参阅 数据协定已知类型

指定使用方法和样式

使用 Web 服务描述语言(WSDL)描述服务时,两种常用样式是文档和远程过程调用(RPC)。 在“文档”样式中,使用架构描述整个消息正文,WSDL 通过引用该架构中的元素来描述各种消息正文部分。 在 RPC 样式中,WSDL 引用每个消息部件的架构类型,而不是元素。 在某些情况下,必须手动选择其中一种样式。 若要执行此操作,可以应用 DataContractFormatAttribute 属性并设置 Style 属性 (Property)(使用 DataContractSerializer 时),或者设置 Style 属性 (Attribute) 上的 XmlSerializerFormatAttribute(使用 XmlSerializer 时)。

此外,XmlSerializer 支持两种形式的序列化 XML:LiteralEncodedLiteral 是最常接受的表单,也是唯一支持的形式 DataContractSerializerEncoded 是 SOAP 规范第 5 节中所述的旧窗体,不建议用于新服务。 若要切换到Encoded模式,请将UseXmlSerializerFormatAttribute属性上的属性设置为 Encoded

在大多数情况下,您不应更改StyleUse属性的默认设置。

控制序列化过程

可以执行许多操作来自定义序列化数据的方式。

更改服务器序列化设置

使用默认值 DataContractSerializer 时,可以通过将属性应用于 ServiceBehaviorAttribute 服务来控制服务上的序列化过程的一些方面。 具体而言,可以使用 MaxItemsInObjectGraph 属性来设置限制 DataContractSerializer 反序列化的最大对象数的配额。 可以使用 IgnoreExtensionDataObject 属性关闭往返版本控制的特性。 有关配额的详细信息,请参阅 数据的安全注意事项。 有关往返的详细信息,请参阅向前兼容的数据协定

[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  

序列化行为

WCF 中提供了两种行为:DataContractSerializerOperationBehaviorXmlSerializerOperationBehavior,它们根据为特定操作使用的序列化程序自动插入。 由于这些行为会自动应用,因此通常不必注意这些行为。

但是,DataContractSerializerOperationBehavior具有MaxItemsInObjectGraphIgnoreExtensionDataObjectDataContractSurrogate等属性,可以用于定制序列化过程。 前两个属性的含义与上一节中所述的含义相同。 可以使用该 DataContractSurrogate 属性启用数据协定代理项,这是用于自定义和扩展序列化过程的强大机制。 有关详细信息,请参阅 数据契约代理

可以使用DataContractSerializerOperationBehavior自定义客户端和服务器的序列化。 以下示例演示如何增加 MaxItemsInObjectGraph 客户端上的配额。

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  

下面是服务上的等效代码,在自托管的情况下:

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

在 Web 托管的情况下,必须创建新的 ServiceHost 派生类,并使用服务主机工厂将其插入。

在配置中控制序列化设置

使用 MaxItemsInObjectGraph 终结点或服务行为,可以通过配置来控制 IgnoreExtensionDataObjectdataContractSerializer,如下面的示例所示。

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

共享类型序列化、对象图保留和自定义序列化器

使用 DataContractSerializer 数据协定名称而不是 .NET 类型名称进行序列化。 这与面向服务的体系结构原则一致,并且具有极大的灵活性-.NET 类型可以更改,而不会影响线路协定。 在极少数情况下,你可能希望序列化实际的 .NET 类型名称,从而在客户端和服务器之间引入紧密耦合,类似于 .NET Framework 远程处理技术。 建议不要采取这种做法,除非在极少数情况下 — 这些情况通常发生在从 .NET Framework 远程处理迁移到 WCF 的过程中。 在这种情况下,必须使用 NetDataContractSerializer 类而不是 DataContractSerializer 类。

通常将 DataContractSerializer 对象图序列化为对象树。 也就是说,如果多次引用同一对象,则会多次序列化它。 例如,请考虑一个PurchaseOrder实例,它有两个类型为“地址”的字段,分别称为billToshipTo。 如果两个字段都设置为同一地址实例,则序列化和反序列化后有两个相同的地址实例。 这样做是因为没有标准的互操作方式来在 XML 中表示对象图(除了前一部分XmlSerializerStyle中提到的可用在Use上的旧版 SOAP 编码标准)。 将对象图序列化为树具有某些缺点,例如,无法序列化具有循环引用的图形。 有时,必须切换到真正的对象图序列化,即使它不可互作。 这可以通过使用 DataContractSerializer,将 preserveObjectReferences 参数设置为 true 来完成。

有时,内置序列化程序不足以满足你的方案。 在大多数情况下,你仍然可以使用XmlObjectSerializer这个抽象,它是DataContractSerializerNetDataContractSerializer的派生来源。

前面的三种情况(.NET 类型保留、对象图保存和完全基于自定义 XmlObjectSerializer的序列化)都需要插入自定义序列化程序。 为此,请执行下列步骤:

  1. 编写自己的派生自 DataContractSerializerOperationBehavior 的行为。

  2. 重写两个 CreateSerializer 方法以返回自己的序列化程序(NetDataContractSerializer、将 DataContractSerializer 设置为 preserveObjectReferencestrue 或自己的自定义 XmlObjectSerializer)。

  3. 在打开服务主机或创建客户端通道之前,请删除现有 DataContractSerializerOperationBehavior 行为,并插入在前面的步骤中创建的自定义派生类。

有关高级序列化概念的详细信息,请参阅 序列化和反序列化

另请参阅