WCF足迹2:契约
(原创:灰灰虫的家http://hi.baidu.com/grayworm)
契约是WCF中很重要的概念。它是用一种与平台无关的标准语法来描述WCF服务的功能。当客户端获取服务端WCF服务的时候,会根据服务端声明的契约生成客户端契约的复本,客户端和服务端通过契约来实现沟通。
一个生活中的例子:
比如KFC,它是一家快餐品牌。假设我想通过加盟的方式在我家附近开一家KFC快餐店。首先,我们要向KFC加盟代理提交加盟申请,经过资格申查后,和KFC签述加盟协议。然后,我根据协议中规定的条款在我家附开了一家KFC快餐店。KFC总部为我提供方法、技术和原材料等,我每年向KFC总部交加盟费,这样附近的朋就可以从我的KFC快餐店中获得KFC的产品和服务了。
“KFC总部”就相当于我们的WCF服务
“来吃KFC附近的朋友”相当于要获取WCF服务的客户端代码
“我开的KFC快餐店”相当于客户端的代理类
“我与KFC签定的加盟协议”相当于WCF的契约。
通过这个例子我们可以看到契约在WCF中的重要性,它就像服务端提供的“加盟协议”一样,客户端根据“加盟协议”中规定的要求在客户端生成代理类(开办加盟店),并根据加盟协议规定的权利从服务端获取服务(获取方法、技术和原材料等),这样客户端在我的加盟店里就可以直接得到KFC服务。
所以说契约是服务端与客户端进行信息交流的基础。
在WCF中包括了四种契约:服务契约,数据契约,错误契约和消息契约。在这里我们重点来看服务契约和数据契约。
在WCF中契约是以Attribute型式进行声明的。
1.用来定义服务契约的两个Attribute:
[AttributeUsage(AttributeTargets.Interface|AttributeTargets.Class,Inherited = false)]
public sealed class ServiceContractAttribute : Attribute
{
public string Name
{get;set;}
public string Namespace
{get;set;}
//More members
}
[AttributeUsage(AttributeTargets.Method)]
public sealed class OperationContractAttribute : Attribute
{
public string Name
{get;set;}
//More members
}
2.用来定义数据契约的两个Attribute:
[AttributeUsage(AttributeTargets.Enum | AttributeTargets.Struct | AttributeTargets.Class, Inherited = false)]
public sealed class DataContractAttribute : Attribute
{
public string Name
{get;set;}
public string Namespace
{get;set;}
}
[AttributeUsage(AttributeTargets.Field|AttributeTargets.Property, Inherited = false)]
public sealed class DataMemberAttribute : Attribute
{
public bool IsRequired
{get;set;}
public string Name
{get;set;}
public int Order
{get;set;}
}
一、服务契约:
1.服务契约的概念
我们可以在接口或者类上声明[ServiceContract]和[OperationContract]来定义服务契约。
[ServiceContract]
interface IMyContract
{
[OperationContract]
string MyMethod(string text);
//MyOtherMethod方法没有声明[OperationContract],不会成为契约的一部份
string MyOtherMethod(string text);
}
class MyService : IMyContract
{
public string MyMethod(string text)
{
return "Hello " + text;
}
public string MyOtherMethod(string text)
{
return "Cannot call this method over WCF";
}
}
ServiceContract声明用来把.NET中的接口声明(CLR格式)映射为与平台无关的契约声明(XML格式),以向外界暴露服务访问入口。ServiceContract声明与类的访问修饰符无关,即不管接口(类)的访问修饰符是public/private/protected/internal,只要把该接口(类)声明为ServiceContract,该接口(类)总会变成服务契约暴露给客户端。因为访问修饰符(public/private/protected/internal)定义的是在CLR中的访问边界,而ServiceContract定义的是在WCF中的访问边界。
在WCF中服务契约接口都需要显示声明为ServiceContract,否则,接口不会被当成WCF契约向外界暴露。
即使我们把接口声明为ServiceContract了,但该服务契约现在并不包含任何成员,我们还要在需要作为契约成员的方法上面加上OperationContractAttribute声明。像上面的代码中,MyMethod方法会作为IMyContract契约的成员向外界暴露,而MyOtherMethod方法则不会成为IMyContract契约的成员。
OperationContract 可以应用在成员方法、属性、索引器和事件上面。
2.ServiceContract的NameSpace属性和Name属性
在编写WCF服务的时候,我们应当为每个服务契约设置NameSpace属性,如果为服务契约指定NameSpace属性的话,那该服务契约会默认NameSpace="http://tempuri.org"。这里NameSpace的作用与原来CLR中NameSpace的作用一样,都是为了定义一个命名空间,防止命名的冲突。
如:
[ServiceContract(Namespace = "http://hi.baidu.com/grayworm")]
interface IMyContract
{...}
对Internet发布的服务契约,命名空间一般使用公司的网址进行命名,对于局域网内发布的服务契约则没有必要按照这种方式进行命名,我们可以使用更有意义单词作为NameSpace。
[ServiceContract(Namespace = "MyNamespace")]
interface IMyContract
{...}
我们还可以为服务契约指定别名。在默认的情况下,服务契约的名称与接口的名称一样,我们可以在ServiceContract声明中使用Name属性为服务契约指定别名。
[ServiceContract(Namespace="")]
public interface ICaculator
{
[OperationContract(Name="AddInt")]
int Add(int arg1, int arg2);
[OperationContract(Name="AddDouble")]
double Add(double arg1, double arg2);
}
测试结果:
《图2》
从图中我们可以看出服务的名子不再是接口的名子了。
3.服务契约中的方法重载
在面向对象的思想中,我们有方法重载的概念,所谓的方法重载就是指一个类中如果两个方法的方法名相同而方法参数不同,那这两个参数就形成了重载
如:
interface ICalculator
{
int Add(int arg1,int arg2);
double Add(double arg1,double arg2);
}
CLR可以根据方法的能数来区分这两个方法。而在WCF世界中这种方法名相同而参数不同的形式则会引发InvalidOperationException异常,即在WCF中不支持面向对象中的方法重载。
如:
//这是种契约定义是错误的
[ServiceContract]
interface ICalculator
{
[OperationContract]
int Add(int arg1,int arg2);
[OperationContract]
double Add(double arg1,double arg2);
}
上面这个服务契约编译的时候是没有问题的,因为它符合CLR的重载要求,但当我们使用HOST发布服务的时候会产生下面的问题:
《图1》
下面我们看一下如何解决ClR和WSDL中不统一的情况:
我们可以在OperationContract声明上通过Name属性为方法起别名,将来客户端就会通过这个别名来区分不同方法的。如:
[ServiceContract]
public interface ICaculator
{
[OperationContract(Name="AddInt")]
int Add(int arg1, int arg2);
[OperationContract(Name="AddDouble")]
double Add(double arg1, double arg2);
}
这样在客户端会把两个Add方法区分为AddInt和AddDouble两个方法。测试结果如下图:
《图3》
4.服务契约的继承
下面我们看一下契约的继承。
我先声明一个简单计算器的契约ISimpleCaculator,只能够作加法运算:
[ServiceContract]
public interface ISimpleCaculator
{
[OperationContract]
int Add(int arg1, int arg2);
}
我们再声明一个科学计算器的契约IScientificCaculator,派生自ISimpleCaculator,在简单计算器的功能之上还能够做乘法运算。
[ServiceContract]
public interface IScientificCaculator:ISimpleCaculator
{
[OperationContract]
int Mutiply(int arg1, int arg2);
}
然后我们再编写一个类实现IScientificCaculator接口。
public class MyCaculator : IScientificCaculator
{
public int Add(int arg1, int arg2)
{
return arg1 + arg2;
}
public int Mutiply(int arg1, int arg2)
{
return arg1 * arg2;
}
}
服务契约编写完成后,我们再在宿主程序中配置终结点:
<service name = "MyCalculator">
<endpoint
address = "http://localhost:8001/MyCalculator/"
binding = "basicHttpBinding"
contract = "IScientificCalculator"
/>
</service>
这样我们就把WCF服务和宿主程序编写好了,下一步就在客户端添加WCF服务的引用。当添加完对WCF服务的引用后,客户端就会通过元数据终结点获取服务契约的信息,并在客户端生成代理代。
代理类的契约声明代码如下:
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
[System.ServiceModel.ServiceContractAttribute(ConfigurationName="SR.IScientificCaculator")]
public interface IScientificCaculator {
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/ISimpleCaculator/Add", ReplyAction="http://tempuri.org/ISimpleCaculator/AddResponse")]
int Add(int arg1, int arg2);
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IScientificCaculator/Mutiply", ReplyAction="http://tempuri.org/IScientificCaculator/MutiplyResponse")]
int Mutiply(int arg1, int arg2);
}
从上面的代码中我们看出,虽然在服务端我们编写了两个有继承关系的契约,但在客户端并没有为我们生成两个对应的契约,而是只生成了一个服务契约,在这个服务契约中包含了两个OperationContract。
这两个OperationContract分别与服务端两个ServiceContract中的OperationContract相对应。由于这两个OperationContract来源于不同的服务契约,所以在OperationContract的属性中有Action="http://tempuri.org/ISimpleCaculator/Add"和Action="http://tempuri.org/IScientificCaculator/Mutiply"两个属性声明,这个Action属性就是映射该OperationContract服务端的OperationContract。
二、数据契约
1.数据契约的概念。
我们在进行WCF编程的时候,服务端程序难免会与客户端程序之间发生数据交换,由于服务端与客户端可能是两种异质运行环境,这就需要实现服务端的数据类型与客户端代理类数据类型的统一。
在服务器端与客户端交换数据是通过流来实现的,因此在传递对象的时候需要我们把对象转换到流中去,在目的地我们再从流中把数据读取出来重新生成能相应对象,这个思想就是我们序列化的思想。在DotNET序列化中是通过Serialization声明来标识类允许被实例化的,这种序列化只是把数据序列化到流中去,而在WCF中不仅仅要把数据序列化到流中去还应包含数据类型的描述。因为客户端的程序可能与服务器端的程序不样而无法实现数据准确的序列化和反序列化,比如服务器端我是用WCF开发的,而客户端是用JavaEE开发的,现在需要从服务器端返回一个Dog对象给客户端。如果只使用简单的序列化和反序列化的话,可能会产生问题:服务器端DotNET序列化的数据在客户端JavaEE不能识别流的格式,无法实现返序列化。
数据契约的作用就是实现一种与平台无关的序列化,即在序列化过程中实现在schema与CLR类型之间转换。
许多内置类型都默认可以被序列化,但自定义类型我们就需要使用数据契约来显式指明其可被序列化。
数据契约使用DataContract和DataMember来声明。
DataContract:修饰可被序列化的数据类型。
DataMember:修饰可被序列化的成员,可以修饰成员变量也可以修饰属性。
[DataContract]
struct Contact
{
[DataMember]
public string FirstName;
[DataMember]
public string LastName;
}
或者
[DataContract]
struct Contact
{
string m_FirstName;
string m_LastName;
[DataMember]
public string FirstName
{
get
{...}
set
{...}
}
[DataMember]
public string LastName
{
get
{...}
set
{...}
}
}
与服务契约一样,使用DataContract和DataMember声明的数据契约也与访问修饰符(public,private,protected...)无关。
2.数据契约的传递
a.命名空间
服务器端定义了一个数据契约,客户端在生成代理类的时候也会生成一个对等的数据契约的复本,但这个契约的复本和服务器端的契约还是有稍许的不同,但命名空间默认是一样的。
如:
服务器端的数据契约定义
namespace MyNamespace
{
[DataContract]
struct Contact
{...}
[ServiceContract]
interface IContactManager
{
[OperationContract]
void AddContact(Contact contact);
[OperationContract]
Contact[] GetContacts( );
}
}
传递到客户端的数据契约复本为:
namespace MyNamespace
{
[DataContract]
struct Contact
{...}
}
[ServiceContract]
interface IContactManager
{
[OperationContract]
void AddContact(Contact contact);
[OperationContract]
Contact[] GetContacts( );
}
如果要想改变传递到客户端的数据契约的命名空间,我们可以在服务器端数据契约声明的时候加上NameSpace属性
namespace MyNamespace
{
[DataContract(Namespace = "MyOtherNamespace")]
struct Contact
{...}
}
这样传递到客户端的数据契约就会变为
namespace MyOtherNamespace
{
[DataContract]
struct Contact
{...}
}
b.DataMember声明的使用
DataMember声明可以加在成员变量上,也可以加在属性上。不管怎样使用DataMember声明,总会在客户端代理类中生成带有DataMember声明的相关属性。
当服务端DataContract成员变量加上DataMember声明的时候,客户端代理类会产生对应的DataMember声明的属性。客户端代理类DataMember属性的名子与服务端DataMember成员变量名子相同,并在客户端动态生成对应的成员变量,成员变量命名是在DataMember属性名子的后面加Feild的形式。
如服务端数据契约的声明如下:
[DataContract]
public class Book
{
[DataMember]
public string BookNO;
[DataMember]
public string BookName;
[DataMember]
public decimal BookPrice;
}
客户端生成代理类中数据契约的声明如下:
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "3.0.0.0")]
[System.Runtime.Serialization.DataContractAttribute(Name="Book", Namespace="http://schemas.datacontract.org/2004/07/Services")]
[System.SerializableAttribute()]
public partial class Book : object, System.Runtime.Serialization.IExtensibleDataObject, System.ComponentModel.INotifyPropertyChanged {
[System.Runtime.Serialization.OptionalFieldAttribute()]
private string BookNOField;
[System.Runtime.Serialization.OptionalFieldAttribute()]
private string BookNameField;
[System.Runtime.Serialization.OptionalFieldAttribute()]
private decimal BookPriceField;
[System.Runtime.Serialization.DataMemberAttribute()]
public string BookNO {
get {...}
set {...}
}
}
[System.Runtime.Serialization.DataMemberAttribute()]
public string BookName {
get {...}
set {...}
}
}
[System.Runtime.Serialization.DataMemberAttribute()]
public decimal BookPrice {
get {...}
set {...}
}
}
//其它的属性和方法
}
当服务端DataContract属性上加上DataMember声明的时候,客户端代理类会产生对应的DataMember声明的属性。客户端代理类DataMember属性的名子与服务端DataMember属性名子相同,并在客户端动态生成对应的成员变量,成员变量命名是在属性名子的后面加Feild的形式。
如服务端数据契约的声明如下:
[DataContract]
public class Book
{
private string _BookNO;
[DataMember]
public string BookNO
{
get { return _BookNO; }
set { _BookNO = value; }
}
private string _BookName;
[DataMember]
public string BookName
{
get { return _BookName; }
set { _BookName = value; }
}
private decimal _BookPrice;
[DataMember]
public decimal BookPrice
{
get { return _BookPrice; }
set { _BookPrice = value; }
}
}
客户端生成代理类中数据契约的声明如下:
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "3.0.0.0")]
[System.Runtime.Serialization.DataContractAttribute(Name="Book", Namespace="http://schemas.datacontract.org/2004/07/Services")]
[System.SerializableAttribute()]
public partial class Book : object, System.Runtime.Serialization.IExtensibleDataObject, System.ComponentModel.INotifyPropertyChanged {
[System.Runtime.Serialization.OptionalFieldAttribute()]
private string BookNOField;
[System.Runtime.Serialization.OptionalFieldAttribute()]
private string BookNameField;
[System.Runtime.Serialization.OptionalFieldAttribute()]
private decimal BookPriceField;
[System.Runtime.Serialization.DataMemberAttribute()]
public string BookNO {
get {...}
set {...}
}
}
[System.Runtime.Serialization.DataMemberAttribute()]
public string BookName {
get {...}
set {...}
}
}
[System.Runtime.Serialization.DataMemberAttribute()]
public decimal BookPrice {
get {...}
set {...}
}
}
//其它的属性和方法
}
凡是被DataMember声明修饰的属性,必须要有get和set访问器,如果没有这两个访问器在调用的时候会产生InvalidDataContractException异常信息,因为在序列化的过程中需要通过get和set访问器来操作对象的数据。
要注意的是不要在成员变量和对应属性上都加上DataMember声明,这样会在客户端代理类中产生重复的成员。
3.数据契约的继承
如果两个数据契约类之间有继承关系,需要在这两个类上面都加上DataContract声明,因为DataContract不能被继承。
如服务端的数据契约声明如下:
[DataContract]
public class Pet
{
private string _Name;
[DataMember]
public string Name
{
get { return _Name; }
set { _Name = value; }
}
private string _Owner;
[DataMember]
public string Owner
{
get { return _Owner; }
set { _Owner = value; }
}
}
[DataContract]
public class Dog:Pet
{
private string _Race;
[DataMember]
public string Race
{
get { return _Race; }
set { _Race = value; }
}
}
如果Dog类中没有声明[DataContract]时,在服务加载运行的时候就会产生InvalidDataContractException异常信息。
4.已知数据类型
在C#面向对象的语法中,可以使用子类对象来替代父类对象,但在WCF中则不允许使用这种替代。
如服务契约PetShop返回Dog数据契约来替代Pet数据契约。
[ServiceContract]
public interface IPetSop
{
[OperationContract]
Pet Sell(string owner,string race);
}
public class PetShop : IPetSop
{
public Pet Sell(string owner,string race)
{
Dog dog = new Dog();
dog.Name = Guid.NewGuid().ToString();
dog.Owner = owner;
dog.Race = race;
return dog;
}
}
在面向对象语法中这种写法完全正确,但在面向服务的语法中这种写法是错误的,在客户端调用的时候会产生如下错误:
《图4》
在WCF中为什么会产生这种怪异的显象呢?因为在WCF服务中产生的对象并不是直接被客户端进行使用的,而是根据数据契约,先把服务端产生的对象序列化,然后通过信道发送给客户端,在客户端再根据契约反序列化对象然后再获取数据。在这个例子中,在服务器端序列化的Pet对象是Dog对象,而在客户端却只了解Pet数据契约,不知道Dog数据契约,所以在反序列化的时候会产生异常。
这个问题不仅仅是个返回类型或参数类型的问题,而是涉及到WCF数据契约究竟能否实现“多态性”的问题。为了解决这个问题,我们引入了“已知数据类型属性”这个概念。“已知数据类型属性”就是在父类中注册一下派生自它的子类。
《天龙八部》节选:
......
段延庆冷笑道:“顺我者昌,逆我者亡”!提起钢杖,便向段誉胸口戳了下去。
忽听得一个女子的声音说到:“天龙寺外,菩提树下,化学邋遢,观音长发!”
段延庆听到“天龙寺外”四字时,钢杖凝在半空不动,待听完这四句话,那钢杖竟不住颤动,慢慢缩了回来。他一回头,与刀白凤的目光相对,只见她眼色中似有千言万语欲待吐露。段延庆心头大震,颤声道:“观……观世音菩萨……”
刀白凤点了点头,低声道:“你……你可知这孩子是谁?”
段延庆脑子中一阵晕眩,瞧出来一片模糊,似乎是回到了二十多年前的一个月圆之夜......
......
看过《天龙八部》的朋友应当对这个情节不陌生。这个例子虽然有些“少儿不宜”,但我感常见它能够比较好地解释“已知数据类型”的作用。这个例子中,刀白凤的那四句话,就是向父类(段延庆)注册了一个子类(段誉)为已知数据类型。
a.在父类数据契约上使用KnownTypeAttribute
“已知数据类型”可以在数据契约的父类上加上[KnownType(Type t)]这个Attribute来实现的。代码如下:
[DataContract]
[KnownType(typeof(Dog))]
public class Pet
{
...
}
[DataContract]
public class Dog:Pet
{
...
}
运行效果:
《图5》
这是因为在客户端生成代理类的时候,会根据[KnownType(typeof(Dog))]声明在客户端同时生成Pet契约和Dog契约,这样客户端可以识点Dog对象,能对其反序列化。
《图6》
b.在父类数据契约的方法中使用ServiceKnownTypeAttribute
上面使用KnownTypeAttribute可以解决子类对象替代父类对象的问题,但它是定义在数据契约类的级别上的,所定义的范畴有些大。
这里我们可以使用ServiceKnownTypeAttribute。ServiceKnownTypeAttribute是定义在服务契约的方法契约级别上的,只有当前方法契约可以识别子类Dog,其它方法中无法识别Dog。
[ServiceContract]
public interface IPetSop
{
[OperationContract]
[ServiceKnownType(typeof(Dog))]
Pet Sell(string owner,string race);
}
public class PetShop : IPetSop
{
public Pet Sell(string owner,string race)
{
Dog dog = new Dog();
dog.Name = Guid.NewGuid().ToString();
dog.Owner = owner;
dog.Race = race;
return dog;
}
}
当[ServiceKnownType(Type t)]被声明在服务契约的级别上时,当前服务契约中的任何方法都可以识别它所指定的子类。
[ServiceContract]
[ServiceKnownType(typeof(Dog))]
public interface IPetSop
{
[OperationContract]
Pet Sell(string owner,string race);
}
public class PetShop : IPetSop
{
public Pet Sell(string owner,string race)
{
Dog dog = new Dog();
dog.Name = Guid.NewGuid().ToString();
dog.Owner = owner;
dog.Race = race;
return dog;
}
}
注:
1.不要把ServiceKnowntypeAttribute声明加在服务类本身上,但可以把ServiceKnowntypeAttribute声明加在接口服务契约中。
2.KnownTypeAttribute(Type t)是用在数据契约中的,而ServiceKnowntypeAttribute(Type t)是用在服务契约中的。
c.使用多个“已知数据类型”
我们可以使用多个KnownTypeAttribute或ServiceKnowntypeAttribute来告诉WCF识别多个子类
服务端代码:
[DataContract]
class Contact
{...}
[DataContract]
class Customer : Contact
{...}
[DataContract]
class Person : Contact
{...}
[ServiceContract]
[ServiceKnownType(typeof(Customer))]
[ServiceKnownType(typeof(Person))]
interface IContactManager
{...}
在这个例子中有两层继承关系,在服务契约IContactManager上我们需要把两级子类都声明为“已知数据类型”,否则会产生异常。
4.使用配置文件指定“已知数据类型”
上面我们可以为WCF服务设置“已知数据类型”,但是当需要把一个新的子类添加为“已知数据类型”时,就需要我们对现有WCF服务进行修改源代码、重新编译、重新布署等操作,为我们服务的可扩展性大打折扣。为了避免这种问题的出现,WCF允许我们把“已知数据类型”配置在宿主程序的配置文件中。
配置代码如下:
<configuration>
<system.runtime.serialization>
<dataContractSerializer>
<declaredTypes>
<add type = "Services.Pet,Services">
<knownType type = "Services.Dog,Services"/>
</add>
</declaredTypes>
</dataContractSerializer>
</system.runtime.serialization>
这样在客户端生成代理类的时候会根据宿主程序上面的配置文件,在客户端代理类中生成对应的Pet数据契约复本和Dog数据契约复本,并在Pet数据契约复本上加上了KnownTypeAttribute声明。
5.枚举类型
枚举类型默认会自动被序列化,枚举类型的值也会自动被包含在数据契约中,所以没有必要在枚举类型上加DataContractAttribute,直接在服务契约中使用就可以了。
在服务契约中使用枚举类型:
enum ContactType
{
Customer,
Vendor,
Partner
}
[DataContract]
struct Contact
{
[DataMember]
public ContactType ContactType;
[DataMember]
public string FirstName;
[DataMember]
public string LastName;
}
当然我们也可以显式地把枚举类型声明为数据契约,首先需要在枚举上声明DataContractAttribute,然后再在枚举值上加上EnumMemberAttribute 声明,而没明显示声明为EnumMemberAttribute的枚举值将不会包含在该数据契约中。
如:
[DataContract]
enum ContactType
{
[EnumMember(Value = "MyCustomer")]
Customer,
[EnumMember]
Vendor,
//由于没加EnumMemberAttribute声明,Partner不会是数据契约中的成员
Partner
}
在客户端生成的代理类的数据契约声明如下:
enum ContactType
{
MyCustomer,
Vendor
}
(原创:灰灰虫的家http://hi.baidu.com/grayworm)