契约是消息参与者之间的约定。在SOA架构中,契约提供了服务通信所必需的元数据。契约用来定义数据类型,操作,消息交换模式和消息交换使用的传输协议。契约通常是在标准化平台中使用与编程语言无关的XML格式发布的。这样做的好处是:允许契约涉及的各方都能够使用和理解契约。在WCF中,服务元数据通常是用WSDL(Web服务描述语言)和XSD(扩展样式定义)文档格式来描述的。在WCF程序中,契约不一定是WSDL和XSD的文档集合,而可能是.NET类型定义的集合,如有需要就可以转化为WSDL和XSD的格式文档。
WCF契约是标注了特定特性的.NET类型,通过这些特定来产生符合行业标准的WSDL和XSD文档。WCF契约会把这些类型映射为服务、操作、消息和消息中的部分。WCF有5种类型的契约:服务契约、操作契约、数据契约、消息契约和错误契约。服务契约映射类型的服务定义,并且映射类型成员到服务操作;数据契约和消息契约映射类型到服务操作的消息定义。与数据契约相比,消息契约提供了对整个消息定义的控制。数据契约会映射为消息类型的消息体成员,而消息契约会映射为消息类型的消息头和消息体成员。
服务契约描述的是一个服务,它定义了 服务向外界公开的功能,这些功能表现为服务操作(Service Operation)。服务契约包括服务定义的各个方面、服务的操作、每个操作的消息交换模式以及每个操作使用的消息。
创建服务契约的第一步就是建立操作的名字和使用的消息交换契约模式。以《[WCF编程]1.WCF入门示例》中为例,服务契约包含三个操作: SayHello、Say和SayOneWay.假设,SayHello和Say使用请求/应答消息交换模式,而SayOneWay使用数据报(单向)消息交换模式,则服务可以定义如下:
class="brush: csharp; auto-links: true; collapse: false; first-line: 1; gutter: true; html-script: false; light: false; ruler: false; smart-tabs: true; tab-size: 4; toolbar: true;">public interface IHelloWorldService { [OperationContract()] string SayHello(string str); [OperationContract] Student Say(); [OperationContract(IsOneWay=true)] void SayOneWay(string str); }与普通的.NET接口定义的差别在于ServiceContractAttribute和OperationContractAttribute。ServiceContractAttribute标注的作用就是告诉WCF基础结构,可以使用该接口作为服务契约。OperationContractAttribute则意味着该方法是服务中的一个操作。
ServiceContractAttribute和OperationContractAttribute类型定义了几个实例属性,当在服务里使用这些属性时,他们会提供对服务契约的控制。
ServiceContractAttribute的类型定义如下:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, Inherited = false, AllowMultiple = false)] public sealed class ServiceContractAttribute : Attribute { public ServiceContractAttribute(); public Type CallbackContract { get; set; } public string ConfigurationName { get; set; } public bool HasProtectionLevel { get; } public string Name { get; set; } public string Namespace { get; set; } public ProtectionLevel ProtectionLevel { get; set; } public SessionMode SessionMode { get; set; } }CallbackContract属性可以在定义双工契约时使用。ConfigurationName属性是配置文件中引用服务的简称。Name和Namespace属性是服务的名称和命名空间,这些是都会序列化到服务的XML数据名称和命名空间中。
ProtectionLevel属性
ProtectionLevel属性表示当使用一个服务契约时,绑定必须达到的消息安全级别。该属性的类型为System.Net.Security.ProtectionLevel,它的三个枚举值是:None,Sign和EncryptAndSign。当System.Net.Security.ProtectionLevel属性的值为Sign时,服务发送和接受的所有消息必须经过签名。当属性设置为None时,表示服务发送和接受的消息无须启用消息安全。ProtectionLevel属性影响的是是消息体中数据,不会影响消息头的结构。
SessionMode属性
SessionMode属性表示程序里使用的通道必须、允许或者不允许使用会话通道。SessionMode属性是System.ServiceModel.SessionMode类型,它的枚举值是Allowed-允许会话、NotAllowed-不允许会话、Required-要求会话(需要有支持会话的Binding支持,WsHttpBinding、NetTcpBinding等)。若要设计一个会话契约,我们建议使用Required 方式,而不是默认值Allowed。SessionMode.Required值要求必须使用传输层会话,但应用层会话却不是必要的,如果服务终结点的绑定没有维持一个传输层会话,就不能为这样的服务契约配置SessionMode.Required。这一约束条件会在装载服务时进行验证。但是,我们仍然可以将服务配置为单调服务,服务实例会在每次客户端调用期间创建与销毁实例。只有当服务被配置为会话服务时,服务实例才会存活于整个客户端会话中。
服务契约包含服务中的描述信息。当描述服务契约中的操作时,有必要先描述操作的消息交换模式、操作接受的消息结构和操作发送的消息结构。如下示例:
[ServiceContract(Namespace="www.cnblogs.com")] public interface IHelloWorldService { [OperationContract()] string SayHello(string str); [OperationContract] Student Say(); [OperationContract(IsOneWay=true)] void SayOneWay(string str); }OperationContractAttribute标注也包含几个可以控制消息交换模式、安全、会话和消息结构的属性。OperationContractAttribute只在方法上有效。
下面是OperationContractAttribute公开的API:
[AttributeUsage(AttributeTargets.Method)] public sealed class OperationContractAttribute : Attribute { public string Action { get; set; } public bool AsyncPattern { get; set; } public bool HasProtectionLevel { get; } public bool IsInitiating { get; set; } public bool IsOneWay { get; set; } public bool IsTerminating { get; set; } public string Name { get; set; } public ProtectionLevel ProtectionLevel { get; set; } public string ReplyAction { get; set; } }AsyncPattern属性
AsyncPattern属性表示操作是否是异步编程模型(APM)的一部分。当这个属性设置为true时,该属性必须应用到Begin/End对中的Begin<方法名>上。End<方法名>不需要使用该属性。如果没有End<方法名>,则契约就不会被使用。当AsyncPattren属性设置为true时,接受基础结构就会异步地调用Begin<方法名>。操作中执行I/O的接收程序要把这个属性设置为true,因为这样可以带来更多的伸缩性。如果一个操作只单纯地执行计算任务,那么该属性就没有必要设为true,因为这会导致一些性能问题。AsyncPattern属性对于发送者来说是完全透明的。
如下所示:
IHelloWorldService(接口):
[ServiceContract(Namespace="www.cnblogs.com")] public interface IHelloWorldService { [OperationContract(AsyncPattern = true)] IAsyncResult BeginRead(string fileName, AsyncCallback userCallback, object stateObject); string EndRead(IAsyncResult asynResult); }HelloWorldService(具体类):
public class HelloWorldService : IHelloWorldService { private const string baseLocation = @"E:\"; private FileStream _stream; private byte[] _buffer; public IAsyncResult BeginRead(string fileName, AsyncCallback userCallback, object stateObject) { this._stream = new FileStream(baseLocation + fileName, FileMode.Open, FileAccess.Read, FileShare.Read); this._buffer = new byte[this._stream.Length]; return this._stream.BeginRead(this._buffer, 0, this._buffer.Length, userCallback, stateObject); } public string EndRead(IAsyncResult ar) { this._stream.EndRead(ar); this._stream.Close(); return Encoding.ASCII.GetString(this._buffer); } }HelloWorldServiceClient(客户端代理类):
public partial class HelloWorldServiceClient : System.ServiceModel.ClientBase<IHelloWorldService>, IHelloWorldService { ...... public string Read(string fileName) { return base.Channel.Read(fileName); } public System.Threading.Tasks.Task<string> ReadAsync(string fileName) { return base.Channel.ReadAsync(fileName); } }客户端生成的服务契约和服务代理类中,会有一个唯一的操作Read。也就是说,不管服务采用同步模式还是异步模式实现,对客户端的服务调用方式没有任何影响,客户端可以任意选择相应的模式进行服务调用。
Program(客户端)class Program { static void Main(string[] args) { string s = client.Read("示例"); Console.Read(); } }IsOneWay属性
默认情况下,所有的操作都使用请求/应答消息交换模式。咋一看,要创建一个单向操作,只要把操作的返回值类型定义为void就可以了。事实上,在接受程序里,如果方法返回被定义为void,则程序会产生一个应答消息,并且这个应答消息的消息体不会包含任何信息。如果想使用数据报(单向)消息交换模式,则在这个方法上再设置IsOneWay属性为ture。推荐大家使用这种交换模式,因为数据包消息交换模式能带来更高的伸缩性,也能满足更高级场合的使用。
显然,错误处理更加适用于请求/应答消息交换模式而不是数据报消息交换模式,这也是开发团队选择选择请求/应答交换模式作为默认交换模式的原因之一。假如出现一个错误,则接受者通过回发通道发送一个错误消息给发送者。在契约中,来自数据报消息交换模式的错误必须通过WS-Addressing FaultTo消息头节点中指定的地址发送给发送者。但是出于安全的原因,默认情况下不会启用这些行为。
IsInitiating和IsTerminating属性
IsInitiating和IsTerminating属性会影响终结点的会话行为。如果IsInitiating属性的值为true,操作接收到一个新消息就会重新启动一个新的会话。如果IsTerminating属性的值为true,操作接收到一个消息时就会终止当前会话。操作可以同时将IsInitiating和IsTerminating属性的值设置为true。只有当ServiceContractAttribute上的SessionMode属性设置为Required时,这些属性才可以设置为true。IsInitiating和IsTerminating属性绝大多数情况下会在会话开始和结束的服务里使用。
Name、Action和ReplyAction属性
Name属性可以获取或设置操作的名称。默认情况下,该属性就是接口或类中的方法名。Action属性让WS-Addressing的action与接受到的消息关联起来,而ReplyAction属性可以把WS-Addressing的action与应答消息关联起来。如果Action属性的值设置为*,则这个操作就可以成为任何一个WS-Addressing消息。这个功能十分有用,尤其是当一个操作要接受各种不同消息时,如消息路由。
数据契约定义了客户端与服务之间信息交换的结构和内容。数据契约会把.NET类型映射到消息体上,而且它是消息序列化和反序列化的关键部分。数据契约可以单独使用,但通常都是在服务契约的操作中使用。数据契约中重要的特性就是DataContractAttribute和DataMemberAttribute 。示例如下所示:
[DataContract] public class Student { [DataMember] public string UserName { get; set; } [DataMember] public int Age { get; set; } [DataMember] public GenderMode Gender { get; set; } }DataContractAttribute特性
DataContractAttribute只能用于枚举、类和结构体,而不能用于接口;DataContractAttribute是不可以被继承的,也就是说当一个类型继承了一个应用了DataContractAttribute特性类型,自身也只有显式地应用DataContractAttribute特性才能成为数据契约;一个类型上只能应用唯一一个DataContractAttribute特性。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, Inherited = false, AllowMultiple = false)] public sealed class DataContractAttribute : Attribute { public DataContractAttribute(); public bool IsReference { get; set; } public string Name { get; set; } public string Namespace { get; set; } }DataContractAttribute仅仅包含3个属性成员。其中Name和Namespace表示数据契约的名称和命名空间;IsReference表示在进行序列化的时候是否保持对象现有的引用结构。比如说,一个对象的两个属性同时引用一个对象,那么有两个序列化方式,一种是在序列化后的XML仍然保留这种引用结构,另一种是将两个属性的值序列化成两份独立的具有相同内容的XML。
DataMemberAttribute特性
DataMemberAttribute特性只能应用类成员,只有应用了DataMemberAttribute特性的字段或者属性成员才能成为数据契约的数据成员。
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = false, AllowMultiple = false)] public sealed class DataMemberAttribute : Attribute { public DataMemberAttribute(); public bool EmitDefaultValue { get; set; } public bool IsRequired { get; set; } public string Name { get; set; } public int Order { get; set; } }Name:数据成员的名称,默认为字段或者属性的名称;
Order:相应的数据成员在最终序列化后的XML出现的位置,Order值越小越靠前,默认值为-1;
IsRequired:表明属性成员是否是必须的成员,默认值为false,表明该成员是可以缺省的;
EmitDefaultValue:表明在数据成员的值等于默认值的情况下,是否还需要将其序列化到最终的XML中,默认值为true,表示默认值会参与序列化。数据契约和数据成员只和是否应用了DataContractAttribute和DataMemberAttribute有关,与类型和成员的存取限制修饰符(public,internal、protected,private等)无关。也就是说,应用了DataMemberAttribute的私有字段或属性成员也是数据契约的数据成员。
消息契约属于高级功能,使用它可以对SOAP头和SOAP体进行更好的控制。此外,消息契约提供了序列化期间进行安全机制的功能。所有的消息契约必须实现一个公开的、无参构造函数。
消息契约使用的标注特性为MessageContractAttribute、 MessageHeaderAttribute、MessageBodyMemberAttribute。示例代码如下:
[MessageContract] public class TestMessage { [MessageHeader] public string Header { get; set; } [MessageBodyMember] public string Body { get; set; } }标注了MessageHeaderAttribute的类成员将会被序列化到消息头中。将类成员放到消息头中的首要原因就是让基础消息结构可以访问这个属性。
MessageContractAttribute特性
通过在一个类或者结构(Struct)上应用MessageContractAttribute使之成为一个消息契约
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)] public sealed class MessageContractAttribute : Attribute { public MessageContractAttribute(); public bool HasProtectionLevel { get; } public bool IsWrapped { get; set; } public ProtectionLevel ProtectionLevel { get; set; } public string WrapperName { get; set; } public string WrapperNamespace { get; set; } }ProtectionLevel和HasProtectionLevel:表示保护级别,在服务契约中已经对保护级别作了简单的介绍,WCF中通过System.Net.Security.ProtectionLevel枚举定义消息的保护级别。一般有3种可选的保护级别:None、Sign和EncryptAndSign;
IsWrapped、WrapperName、WrapperNamespace:IsWrapped表述的含义是是否为定义的主体成员(一个或者多个)添加一个额外的根节点。WrapperName和WrapperNamespace则表述该根节点的名称和命名空间。IsWrapped、WrapperName、WrapperNamespace的默认是分别为true、类型名称和http://tempuri.org/。MessageHeaderAttribute特性
MessageHeaderAttribute用于定义消息报头成员
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = false)] public class MessageHeaderAttribute : MessageContractMemberAttribute { public MessageHeaderAttribute(); public string Actor { get; set; } public bool MustUnderstand { get; set; } public bool Relay { get; set; } }Actor:表示处理该报头的目标节点(SOAP Node),SOAP1.1中对应的属性(Attribute)为actor,SOAP 1.2中就是我们介绍的role属性;
MustUnderstand:表述Actor(SOAP 1.1)或者Role(SOAP 1.2)定义的SOAP节点是否必须理解并处理该节点。对应的SOAP报头属性为mustUnderstand;
Relay:对应的SOAP报头属性为relay,表明该报头是否需要传递到下一个SOAP节点。MessageBodyMemberAttribute特性
MessageBodyMemberAttribute用于定义消息主体成员
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = false)] public class MessageBodyMemberAttribute : MessageContractMemberAttribute { public MessageBodyMemberAttribute(); public int Order { get; set; } }MessageBodyMemberAttribute的定义显得尤为简单,仅仅具有一个Order对象,用于控制成员在SOAP消息主体中出现的位置。默认的排序规则是基于字母排序。