本主题介绍什么是服务协定、如何定义服务协定、可用的操作(以及对基础消息交换的影响)、所用的数据类型,以及设计满足方案要求的操作时需要考虑的其他问题。
创建服务协定
服务公开一系列操作。 在Windows Communication Foundation (WCF)应用程序中,创建方法并标记为OperationContractAttribute属性,从而定义操作。 然后,若要创建服务协定,需要将操作组合到一起,具体方法是在使用 ServiceContractAttribute 属性标记的接口中声明这些操作,或在使用同一属性进行标记的类中定义它们。 (有关基本示例,请参阅 How to: Define a Service Contract.)
没有 OperationContractAttribute 属性的任何方法都不是服务作,也不会由 WCF 服务公开。
本主题介绍设计服务协定时的以下决策点:
是使用类还是接口。
如何指定要交换的数据类型。
可以使用的交换模式类型。
是否可以将显式安全要求纳入合同的一部分。
操作输入和输出的限制。
类或接口
两个类和接口都表示功能的分组,因此,两者都可用于定义 WCF 服务协定。 但是,建议使用接口,因为它们直接为服务协定建模。 没有实现的话,接口仅仅只是定义具有特定签名的方法的集合。 实现服务协定接口,并且已实现 WCF 服务。
托管接口的所有优势都适用于服务协定接口:
服务协定接口可以扩展任意数量的其他服务协定接口。
单个类可以通过实现这些服务协定接口来实现任意数量的服务协定。
可以通过更改接口实现来修改服务协定的实现,而服务协定保持不变。
可以通过实现旧接口和新接口来对服务进行版本控制。 旧客户端连接到原始版本,而较新的客户端可以连接到较新版本。
注释
从其他服务协定接口继承时,不能重写操作属性,例如名称或命名空间。 如果您尝试这样做,会在当前服务合同中产生新的操作。
有关使用接口创建服务协定的示例,请参阅 How to: Create a Service with a Contract Interface.
但是,可以使用类来定义服务协定并同时实现该协定。 可以通过直接向类和类上的方法分别应用 ServiceContractAttribute 和 OperationContractAttribute 来创建服务,这种方法的优点是快速且简便。 缺点是托管类不支持多个继承,因此它们一次只能实现一个服务协定。 此外,对类或方法签名的任何修改都会修改该服务的公共协定,这可能会阻止未修改的客户端使用服务。 有关详细信息,请参阅 “实现服务协定”。
有关使用类创建服务协定并同时实现它的示例,请参阅 How to: Create a Service with a Contract Class.
此时,应了解使用接口和类定义服务协定之间的区别。 下一步是决定服务与其客户端之间可以交换哪些数据。
参数和返回值
每个操作都有一个返回值和一个参数,即使它们为 void
。 但是,与本地方法不同的是,本地方法可以将对象的引用从一个对象传递到另一个对象,而服务操作则不会传递对象的引用。 它们传递的只是对象的副本。
这很重要,因为参数或返回值中使用的每种类型都必须可序列化;也就是说,必须将该类型的对象转换为字节流,并将字节流转换为对象。
默认情况下,基元类型可序列化,就像 .NET Framework 中的许多类型一样。
注释
操作签名中的参数名称值是协定的一部分且区分大小写。 如果要在本地使用相同的参数名称,但修改已发布元数据中的名称,请参阅 System.ServiceModel.MessageParameterAttribute。
数据协定
面向服务的应用程序(如 Windows Communication Foundation(WCF)应用程序旨在与Microsoft和非Microsoft平台上尽可能多的客户端应用程序进行互作。 为了实现最广泛的互操作性,建议将您的类型用 DataContractAttribute 和 DataMemberAttribute 属性进行标记,以创建数据契约。数据契约是服务契约的一部分,用于描述您的服务操作交换的数据。
数据协定是可选的样式协定:除非您显式应用数据协定属性,否则不会序列化任何类型或数据成员。 数据协定与托管代码的访问范围无关:可以序列化专用数据成员并将其发送到其他位置以公开访问。 (有关数据协定的基本示例,请参阅 如何:为类或结构创建基本数据协定。) WCF 处理支持操作功能的基础 SOAP 消息的定义,以及将您的数据类型序列化到消息正文中和从中序列化出来。 只要数据类型可序列化,就无需在设计作时考虑基础消息交换基础结构。
尽管典型的 WCF 应用程序使用 DataContractAttribute 和 DataMemberAttribute 属性来创建作的数据协定,但可以使用其他序列化机制。 标准 ISerializable、 SerializableAttribute机制和 IXmlSerializable 机制都可用于处理数据类型的序列化,并将其从一个应用程序传递到另一个应用程序的基础 SOAP 消息中。 如果数据类型需要特殊支持,则可以采用更多序列化策略。 有关 WCF 应用程序中数据类型序列化的选项的详细信息,请参阅 在服务协定中指定数据传输。
将参数和返回值映射到消息交换
除了应用程序支持特定标准安全、事务和与会话相关的功能时所需的数据之外,对应用程序数据进行往返传输的 SOAP 消息的基础交换还支持服务操作。 由于这种情况,服务操作的签名决定了一种基础的 消息交换模式(MEP),该模式能够支持数据传输并满足操作所需的功能。 可以在 WCF 编程模型中指定三种模式:请求/回复、单向和双工消息模式。
请求/答复
请求/回复模式是请求发送方(客户端应用程序)接收与请求关联的回复的模式。 这是默认的 MEP,因为它既支持传入操作(一个或多个参数传递到该操作中),也支持将返回值传回给调用方。 例如,以下 C# 代码示例演示一个基本服务操作,该操作接收一个字符串并返回一个字符串。
[OperationContractAttribute]
string Hello(string greeting);
下面是等效的 Visual Basic 代码。
<OperationContractAttribute()>
Function Hello (ByVal greeting As String) As String
此操作签名决定了底层消息交换的形式。 如果不存在关联,WCF 无法确定返回值的用途。
请注意,除非您指定不同的底层消息模式,否则即使是返回void
(在 Visual Basic 中为Nothing
)的服务操作都是请求/回复消息交换。 这个操作的结果是,除非客户端异步调用该操作,否则客户端将停止处理,直到收到返回消息,即便此消息通常情况下是空的。 以下 C# 代码示例演示在客户端收到一个空消息作为响应之前,操作不会返回。
[OperationContractAttribute]
void Hello(string greeting);
下面是等效的 Visual Basic 代码。
<OperationContractAttribute()>
Sub Hello (ByVal greeting As String)
如果操作需要很长时间才能执行,上面的示例可能会降低客户端的性能和响应能力,但即使它们返回 void
,请求/回复操作仍有其优势。 最明显的一个是 SOAP 错误可以在响应消息中返回,这表示在通信或处理过程中发生了一些与服务相关的错误情况。 服务协定中指定的 SOAP 错误作为对象传递给客户端应用程序 FaultException<TDetail> ,其中类型参数是服务协定中指定的类型。 这使得通知客户端 WCF 服务中的错误情况变得简单。 有关异常、SOAP 错误和错误处理的详细信息,请参阅 “协定和服务”中的“指定和处理错误”。 若要查看请求/回复服务和客户端的示例,请参阅 How to: Create a Request-Reply Contract。 有关请求-回复模式问题的详细信息,请参阅 Request-Reply 服务。
单向
如果 WCF 服务应用程序的客户端不需要等待操作完成并且不处理 SOAP 故障,则该操作可以指定使用单向消息模式。 单向操作是指客户端调用某个操作后,WCF将消息写入网络,然后客户端继续处理的过程。 通常这意味着,除非在出站消息中发送的数据非常大,否则客户端几乎立即继续运行(除非发送数据时出错)。 这种类型的消息交换模式支持从客户端到服务应用程序的事件类似行为。
发送一条消息而未接收任何消息的消息交换无法支持指定非 void
的返回值的服务操作;在这种情况下,将引发一个 InvalidOperationException 异常。
没有返回消息还意味着可能没有返回表明处理或通信中任何错误的 SOAP 错误。 (当操作是单向操作时,传达错误信息需要双向消息交换模式。)
若要为返回 void
的操作指定单向消息交换,请将 IsOneWay 属性设置为 true
,如以下 C# 代码示例所示。
[OperationContractAttribute(IsOneWay=true)]
void Hello(string greeting);
下面是等效的 Visual Basic 代码。
<OperationContractAttribute(IsOneWay := True)>
Sub Hello (ByVal greeting As String)
此方法与前面的请求/回复示例相同,但将属性设置为IsOneWaytrue
表示虽然该方法相同,但服务作不会发送返回消息,客户端在出站消息移交给通道层后立即返回。 有关示例,请参阅 “如何:创建 One-Way 协定”。 有关单向模式的详细信息,请参阅 One-Way 服务。
双工
双向模式的特点是,无论使用单向消息还是请求/答复消息,服务和客户端都能够各自独立地向对方发送消息。 这种形式的双向通信对于必须直接与客户端通信的服务或向消息交换的任一方提供异步体验(包括类似事件的行为)非常有用。
由于存在与客户端通信的附加机制,双向模式比请求/答复或单向模式要略为复杂。
若要设计双工协定,还必须设计回调协定,并将该回调协定的类型分配给标记服务协定的 CallbackContract 属性 (Attribute) 的 ServiceContractAttribute 属性 (Property)。
若要实现双工模式,您必须创建第二个接口,该接口包含在客户端调用的方法声明。
有关创建服务的示例以及访问该服务的客户端,请参阅 How to: Create a Duplex Contract and How to: Access Services with a Duplex Contract. 有关工作样例,请参阅 Duplex。 有关使用双工协定的问题的详细信息,请参阅双工服务。
谨慎
当服务接收双工消息时,它会在该传入消息中查找 ReplyTo
元素,以确定要发送答复的位置。 如果用于接收消息的通道不受保护,则不受信任的客户端可能会发送恶意消息给目标计算机从而导致该目标计算机的 ReplyTo
拒绝服务攻击(DoS攻击)。
Out 和 Ref 参数
在大多数情况下,可以使用in
参数(ByVal
在 Visual Basic 中)和out
ref
参数(ByRef
在 Visual Basic 中)。 由于参数out
和ref
都指示从操作返回数据,因此,像下面这样的操作签名指定了请求/答复操作是必要的,即使操作签名返回void
值。
[ServiceContractAttribute]
public interface IMyContract
{
[OperationContractAttribute]
public void PopulateData(ref CustomDataType data);
}
下面是等效的 Visual Basic 代码。
<ServiceContractAttribute()> _
Public Interface IMyContract
<OperationContractAttribute()> _
Public Sub PopulateData(ByRef data As CustomDataType)
End Interface
唯一的例外是签名具有特定结构的情况。 例如,只有当用于声明操作的方法返回NetMsmqBinding时,才能使用void
绑定来与客户端进行通信;不能有任何输出值,无论是返回值、ref
还是out
参数。
此外,使用out
或ref
参数要求操作具有基础响应消息,以便携带回已修改的对象。 如果操作是单向的,那么在运行时将会引发一个 InvalidOperationException 异常。
在协定上指定消息保护级别
设计合同时,还必须确定实现合同的服务的消息保护级别。 仅当消息安全性应用于协定终结点中的绑定时,才有必要这样做。 如果绑定关闭了安全功能(也就是说,如果系统提供的绑定将 System.ServiceModel.SecurityMode 设置为 SecurityMode.None 值),那么您无需为协定决定消息保护级别。 在大多数情况下,应用了消息级安全性的系统提供的绑定提供了足够的保护级别,无需考虑每个作或每条消息的保护级别。
保护级别是一个值,该值指定支持服务的消息(或消息部件)是签名、签名和加密的,还是不使用签名或加密发送的。 可以在各种范围内设置保护级别:在服务级别、特定操作、操作中的消息或消息部分。 在一个范围设置的值会成为比该范围小的范围的默认值(除非显式重写该值)。 如果绑定配置无法提供协定中要求的最小保护级别,则将引发异常。 如果在协定上未显式设置保护级别值,则绑定配置控制所有消息的保护级别(如果绑定具有消息安全性)。 这是默认行为。
重要
决定是否将合同的各种范围显式设置为小于ProtectionLevel.EncryptAndSign的完全保护级别,通常涉及在某种程度的安全性和性能提升之间进行权衡。 在这些情况下,您的决策必须围绕您的业务操作及其交换的数据价值展开。 有关详细信息,请参阅 “保护服务”。
例如,下面的代码示例未在协定上设置 ProtectionLevel 或 ProtectionLevel 属性。
[ServiceContract]
public interface ISampleService
{
[OperationContractAttribute]
public string GetString();
[OperationContractAttribute]
public int GetInt();
}
下面是等效的 Visual Basic 代码。
<ServiceContractAttribute()> _
Public Interface ISampleService
<OperationContractAttribute()> _
Public Function GetString()As String
<OperationContractAttribute()> _
Public Function GetData() As Integer
End Interface
当使用默认的 ISampleService
(其默认的 WSHttpBinding 是 System.ServiceModel.SecurityMode)在终结点中与 Message 实现交互时,将对所有消息加密并签名,因为这是默认的保护级别。 但是,当ISampleService
服务与默认BasicHttpBinding(即默认SecurityMode,它是None)一起使用时,所有消息都作为文本发送,因为此绑定没有安全性,因此会忽略保护级别(也就是说,消息既未加密,也未签名)。 如果将SecurityMode更改为Message,那么这些消息将被加密和签名(因为这将成为绑定的默认保护级别)。
如果要显式指定或调整协定的保护要求,请将 ProtectionLevel 属性(或较小范围内的任何 ProtectionLevel
属性)设置为服务协定所需的级别。 在这种情况下,使用显式设置要求绑定在所用的最小范围内支持该设置。 例如,下面的代码示例为ProtectionLevel操作显式指定一个GetGuid
值。
[ServiceContract]
public interface IExplicitProtectionLevelSampleService
{
[OperationContractAttribute]
public string GetString();
[OperationContractAttribute(ProtectionLevel=ProtectionLevel.None)]
public int GetInt();
[OperationContractAttribute(ProtectionLevel=ProtectionLevel.EncryptAndSign)]
public int GetGuid();
}
下面是等效的 Visual Basic 代码。
<ServiceContract()> _
Public Interface IExplicitProtectionLevelSampleService
<OperationContract()> _
Public Function GetString() As String
End Function
<OperationContract(ProtectionLevel := ProtectionLevel.None)> _
Public Function GetInt() As Integer
End Function
<OperationContractAttribute(ProtectionLevel := ProtectionLevel.EncryptAndSign)> _
Public Function GetGuid() As Integer
End Function
End Interface
实现此 IExplicitProtectionLevelSampleService
协定并且具有使用默认 WSHttpBinding(其默认的 System.ServiceModel.SecurityMode 是 Message)的终结点的服务具有以下行为:
操作
GetString
消息是经过加密和签名的。这些
GetInt
操作消息以未加密和未签名的纯文本形式发送。GetGuid
操作 System.Guid 将在一条已加密且签名的消息中返回。
有关保护级别以及如何使用保护级别的详细信息,请参阅 “了解保护级别”。 有关安全性的详细信息,请参阅 “保护服务”。
其他操作签名要求
某些应用程序的功能需要特定的操作签名。 例如,NetMsmqBinding 绑定支持持久性服务和客户端,即应用程序可以在通信期间重新启动,并在其停止的位置处拾取,不会遗漏任何消息。 (有关详细信息,请参阅 WCF 中的队列。但是,持久作必须仅采用一个 in
参数,并且没有返回值。
另一个示例是在操作中使用 Stream 类型。 由于该 Stream 参数包括整个消息正文,因此,如果输入或输出(即 ref
参数、 out
参数或返回值)的类型 Stream,则它必须是作中指定的唯一输入或输出。 此外,参数或返回类型必须为Stream或 System.ServiceModel.Channels.MessageSystem.Xml.Serialization.IXmlSerializable。 有关流的详细信息,请参阅大型数据和流式处理。
名称、命名空间和模糊处理
合约和操作的定义中 .NET 类型的名称和命名空间在合约转换为 WSDL 以及创建和发送合约消息时非常重要。 因此,强烈建议使用Name
Namespace
所有支持协定属性(如 ServiceContractAttribute、OperationContractAttribute、DataContractAttribute、DataMemberAttribute)和其他协定属性的属性显式设置服务协定名称和命名空间。
其中一个结果是,如果未显式设置名称和命名空间,则程序集上的 IL 混淆会更改契约类型名称和命名空间,并导致修改后的 WSDL 和线路交换通常会失败。 如果不显式设置协定名称和命名空间,但确实打算使用模糊处理,请使用 ObfuscationAttribute 和 ObfuscateAssemblyAttribute 属性来防止修改协定类型名称和命名空间。