版本化数据契约
变化是不可避免的。业务的改变,技术的改变,规则的改变,这些都会造成软件契约的变化。面对软件的变化,一个稳定的版本策略是必不可少的。必须注意对不可避免的变化进行预先的筹划,并对当前的客户端保证向后的兼容性。
最常见的对版本化契约的需求是,当现有数据契约中添加新的成员时。按照不间断的版本更改的描述,你可以自由更改,而不需要终止现有的客户端。但如果你需要终止向后兼容的现有客户端,就必须版本化整个数据契约或命名空间。
对于不间断的更新,有一点我们必须小心。不间断,从WCF的角度来看,很可能会破坏与其它系统之间的兼容性。例如,如果一个通信系统,需要严格的结构验证,那么如果该系统收到包含意想不到的元素的XML实例,该系统可能会拒绝这个消息。术语“不间断”,在这里的意思是说执行对数据契约的更改时可以不影响WCF的正常通信。
不间断更改
两种类型的修改将不会中断与现有客户端的兼容性:
- 添加新的非必须的数据成员
- 移出现有的非必须的数据成员
在这两种情况下,有可能在一个新的消息中简单的忽略新的数据成员或丢失非必须的数据来创建一个旧的类型。相反,它也可能从旧的类型创建一个新的类型。DataContractSerializer会在运行时,自动完成这些操作。
间断更改
虽然你可以更改向后兼容的数据契约中的某些属性,但是,许多项目的变化会间断现有的客户端。如果你对数据契约做出任何改变,那么现有客户端可能不再正常工作:
- 改变数据契约的名字或命名空间。
- 重命名以前必须的数据成员。
- 以已使用的名称添加一个新的数据成员。
- 改变现有数据成员的数据类型。
- 添加的新成员的DataMember属性中定义了IsRequired=true。
- 要移出的新成员的DataMember属性中定义了IsRequired=true。
下面代码显示了两个数据契约的定义:第一个定义在V1服务上,第二个定义在V2服务上。注意,在V1和V2之间,数据成员Currency被移出,而DailyVolume被添加上来。这种改变是非中断的
1: namespace WcfServiceLibraryDataContract2: {
3: [DataContract(Namespace = "http://WcfServiceLibraryDataContract")]4: public class clsStockPrice:clsPrice //V15: {
6: [DataMember]
7: public string Ticker;8: [DataMember]
9: public string Currency;10: }
11: }
1: namespace WcfServiceLibraryDataContract2: {
3: [DataContract(Namespace = "http://WcfServiceLibraryDataContract")]4: public class clsStockPrice:clsPrice //V25: {
6: [DataMember]
7: public string Ticker;8: [DataMember]
9: public long DailyVolume;10: }
11: }
对于现有的客户端,要想在新的数据成员改编后,仍可传递正确的数据,那么原始的数据契约必须支持扩展性。也就是说,原始的数据必须支持对未知类型的序列化。也就是说,客户端传递V2的数据到V1服务上,并且V1的服务返回V1的数据到V2的客户端时,元素依然完好。WCF可以通过svcutil.exe生成的默认的代理代码来实现扩展性。如果你不想支持此功能,可以通过在ServiceBehavior配置节中定义<dataContractSerializer ignoreExtensionDataObject=”true”/>来禁用它。
下面的代码显示了客户端调用GetPrice来获得StockPrice对象,然后传递这个对象到StoreStockPrice。假设,StockService代理是使用svcutil.exe生成的指向V1 服务的。然后这个服务被更改为V2。当客户端针对V2服务运行时,GetPrice将会以DailyVolume成员返回XML,并删除其中的Currency成员。已知V1的StockPrice对象的反序列化器,会在对象的ExtensionData字段中替换DailyVolume成员。并且,不在意丢失的Currency成员。客户端代码将会获取到期望的StockPrice对象,却发现它包含Currency的默认值,而且整个对象有些“重”。这是因为额外扩展的数据(DailyVolume)在类中也是可用的。这样,服务可以传递有效的V2数据,并且客户端预定的是一个有效的V1数据的表现形式。
1: localhost.StockServiceClient proxy = new localhost.StockServiceClient()2: localhost.StockPrice s = proxy.GetPrice("Chinasofti");3: proxy.StoreStockPrice(s);
数据契约的等价
如果你使用WCF来暴露服务以及使用svcutil.exe来生成服务的访问者代理,你通常不需要关心客户端和服务端之间传递的消息的表现形式。数据契约直接由WCF序列化.NET类型到XML结构中,并且反序列化一个XML结构回.NET类型。XML信息在通信时可能在通信线路上编码为文本或二进制形式。但同样的,.NET代码也不知道编码形式。这样,在代码中操作.NET类型,而在线路上传递的却是基于标准XML信息的编码表现形式。
但是,有些情况下,你可能在服务端和客户端使用不同的类型。如果客户端和服务端由不同的机构开发出来,或者只有一方的通信使用WCF时,这种情况可能发生。事实上,如果你不使用svctuil.exe或者添加服务引用来在客户端生成代理类,这就有机会产生客户端的成员名和服务器端的成员名不同的情况。但是通过使用[DataMember]属性控制这些名称,使他们在XML的表现形式中保持一致。只要客户端和服务端使用相同的XML表现形式,WCF就不会反序列化XML信息为不同的.NET类型。如果两个类序列化后,是相同的XML结构,数据契约就认为这两个类是相等的。对于数据契约的相等来说,他们必须有相同的命名空间和成员。数据成员也必须是相同的类型,在XML中也必须具有相同的顺序。总之,他们必须没有任何差别。下面代码显示了两个相等的数据契约,第一个契约被暴露在服务中,第二个契约是由客户端描述的。两个契约在生成XML结构后是相等的。默认情况下,服务中的契约,WCF将会按照字母的顺序排列。因为DataContract属性和DataMember中Name=”StockPriceSvc” 和Name=“Currency”属性的定义,第二个契约生成XSD后与第一个契约完全一致。
1: [DataContract(Namespace = "http://WcfServiceLibraryDataContract")]2: public class clsStockPriceSvc3: {
4: [DataMember]
5: public double CurrentPrice;6: [DataMember]
7: public DateTime CurrentTime;8: [DataMember]
9: public string Currency;10: [DataMember]
11: public string Ticker;12:
13: }
1: [DataContract(Namespace = "http://WcfServiceLibraryDataContract", Name="clsStockPriceSvc")]2: public class clsStockPrice3: {
4: [DataMember(Order=4)]
5: public string Ticker;6: [DataMember(Order=2)]
7: public double CurrentPrice;8: [DataMember(Order=3)]
9: public DateTime CurrentTime;10: [DataMember(Order=1,Name="Currency")]11: public string Money;12: }
使用集合
集合是在.NET中是结合了动态内存分配、枚举、以及列表导航等优点的非常方便的数据结构。虽然有用,但没有XSD或WSDL与集合相等。因此,为了序列化一个集合到XML,WCF将他们视为数组。事实上,在线路级别的序列化上,集合与数组是相同的。除了集合(实现了ICollection<T>的类型),实现IEnumerable<T>或IList<T>的类型也是如此处理。
下面代码显示了一个使用集合的服务契约的一个操作。集合以[CollectionDataContract]属性修饰。这个属性专门为处理集合而定义。这个属性告诉WCF实例化任何支持IEnumerable和以及在数组中实现Add方法的类型。StockPriceCollection类继承自List泛型,这个泛型基于ICollection接口。所以,这个类型可以被序列化。
1: [DataContract(Namespace = "http://WcfServiceLibraryDataContract")]2: public class clsStockPrice3: {
4: [DataMember]
5: public double CurrentPrice;6: [DataMember]
7: public DateTime CurrentTime;8: [DataMember]
9: public string Currency;10: [DataMember]
11: public string Ticker;12:
13: }
1: [CollectionDataContract]
2: public class clsStockPriceCollection: List<clsStockPrice>3: {
4: }
1: [ServiceContract(Namespace = "http://WcfServiceLibraryDataContract/FinanceService/")]2: public interface IStockService3: {
4: [OperationContract]
5: clsStockPriceCollection GetPricesAsCollection(string[] tickers);6: }
1: [ServiceBehavior(Namespace = "http://WcfServiceLibraryDataContract/FinanceService/")]2: public class StockService : IStockService3: {
4: #region IStockService Members5:
6: clsStockPriceCollection GetPricesAsCollection(string[] tickers)7: {
8: clsStockPriceCollection list = new clsStockPriceCollection();9: for (int i = 0; i < tickers.GetUpperBound(0) + 1; i++)10: {
11: clsStockPrice p = new clsStockPrice();12: p.Ticker = tickers[i];
13: p.CurrentPrice = 94.85;
14: p.CurrentTime = DateTime.Now;
15: list.Add(p);
16: }
17: return list;18: }
19:
20: #endregion21: }