随着版本的不断演化,客户端与服务端的数据契约可能会出现版本不一致的情况。在WCF中,关于数据契约的版本控制有两种情况:新增成员与缺失成员。新增成员是指发送方包含了新增成员,默认处理方式为忽略新增成员。缺失成员则是指发送方缺少成员,默认处理方式是为缺失成员赋予其默认值。
在缺失成员的情况下,如果仅仅是为缺少的成员赋予默认值,有时候会出现无法预料的错误。原因在于缺失的成员有可能是正确执行操作的必要条件。为了避免出现这样的情况,可以将缺失的成员设置为必备成员,方法是利用DataMember特性的IsRequired属性,将其值设置为true。例如:
[DataContract]
struct Contact { [DataMember]
public string FirstName;
[DataMember] public string LastName; [DataMember(IsRequired = true)]
public string Address; }
如果消息中的成员被标记为必备成员,当接收端的DataContractSerializer无法找到所需的信息进行反序列化时,就会取消这次调用,发送端会引发NetDispatcherFaultException异常。例如,服务端的数据契约如上的定义,其中Address字段为必备成员,而客户端的数据契约则如下所示:
[DataContract]
struct Contact { [DataMember]
public string FirstName;
[DataMember] public string LastName; }
此时,如果客户端向服务发出调用,则由于引发了异常,该调用就不会到达服务。
客户端和服务都能够将它们的数据契约中的部分或所有数据成员标记为必备,彼此之间是完全独立的。被标记为必备的成员越多,则与服务或客户端之间的交互就越安全,但这却是以牺牲灵活性与版本兼容性为代价的。
本书总结了数据契约版本控制的几种情形,并以表显示了必备成员的版本兼容性:
IsRequired |
V1 to V2 |
V2 to V1 |
False |
Yes |
Yes |
True |
No |
Yes |
假定V2包含了V1的所有数据成员,同时还定义了新增成员。则表涵盖了版本的几种情况。
V1到V2:代表了缺失成员的情况。如果IsRequired为false,则交互正常,对于缺失成员则设置为默认值。如果IsRequired为true,就会抛出异常,消息不能正常发送。
V2到V1:代表了新增成员的情况。不管IsRequired的值为true还是false,WCF均以忽略新成员的方式进行交互,交互正常。
WCF对于一些特殊的数据类型,支持仍然不够。这在一定程度上限制了CLR开发人员对WCF的设计。这些特殊的数据类型包括:枚举、委托、DataSet和DataTable、泛型、集合。
枚举
WCF对枚举的支持还算不错。首先,枚举类型自身是支持序列化的。不需要设置任何特性,枚举类型的所有成员都会是数据契约的一部分。如果,枚举类型中只有一部分成员需要成为数据契约的一部分,就需要用到DataContract与EnumMember特性。例如:
[DataContract]
enum ContactType { [EnumMember]
Customer, [EnumMember] Vendor,
//Will not be part of data contract
Partner }
客户端生成的表示形式则为:
enum ContactType { Customer, Vendor }
委托
WCF对委托以及事件的支持都不够好。这是因为委托的内部调用列表的具体结构是本地的,客户端或服务无法跨服务边界共享委托列表的结构。此外,我们不能保证内部列表中的目标对象都是可序列化的,或者都是有效的数据契约。这会导致序列化的操作时而成功,时而失败。因此,最佳实践是不要将委托成员或事件作为数据契约的一部分。
数据集与数据表
DataSet和DataTable类型是可序列化的,因而我们可以在服务契约中接收或返回数据表或数据集。
如果服务契约使用了DataSet和DataTable类型,生成的代理文件不会直接使用DataSet和DataTable类型,而是包含DataTable数据契约的定义(只包含DataTable的样式,而不包含任何代码)。但我们可以手工修改这些定义。例如这样的服务契约:
[ServiceContract()]
public interface IContactManager { [OperationContract] void AddContact(Contact contact); [OperationContract]
void AddContacts(DataTable contacts);
[OperationContract] DataTable GetContacts(); }
那么生成的代理文件可能会是这样:
public interface IContactManager { [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IContactManager/AddContact",
ReplyAction="http://tempuri.org/IContactManager/AddContactResponse")]
[System.ServiceModel.XmlSerializerFormatAttribute()]
void AddContact(Contact contact); [System.ServiceModel.OperationContractAttribute(Action= "http://tempuri.org/IContactManager/AddContacts", ReplyAction="http://tempuri.org/IContactManager/AddContactsResponse")]
[System.ServiceModel.XmlSerializerFormatAttribute()] AddContactsResponse AddContacts(AddContactsRequest
request); [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IContactManager/GetContacts",
ReplyAction="http://tempuri.org/IContactManager/GetContactsResponse")]
[System.ServiceModel.XmlSerializerFormatAttribute()] GetContactsResponse GetContacts(GetContactsRequest
request); } 代理类的定义则如下所示: [System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")] public partial class ContactManagerClient : System.ServiceModel.ClientBase<IContactManager>, IContactManager { //其余成员略;
public void AddContact(Contact contact) {
base.Channel.AddContact(contact);
} AddContactsResponse IContactManager.AddContacts(AddContactsRequest request)
{ return base.Channel.AddContacts(request); } public void AddContacts(AddContactsContacts contacts) { AddContactsRequest inValue = new AddContactsRequest();
inValue.contacts = contacts; AddContactsResponse retVal = ((IContactManager)(this)).AddContacts(inValue); } GetContactsResponse IContactManager.GetContacts(GetContactsRequest request)
{ return base.Channel.GetContacts(request); } public GetContactsResponseGetContactsResult GetContacts() { GetContactsRequest inValue = new GetContactsRequest(); GetContactsResponse retVal = ((IContactManager)(this)).GetContacts(inValue);
return retVal.GetContactsResult; } }
我们可以手动将AddContacts()与GetContacts()方法修改为:
public void AddContacts(DataTable contacts) { AddContactsRequest inValue = new AddContactsRequest(); inValue.contacts = contacts; AddContactsResponse retVal = ((IContactManager)(this)).AddContacts(inValue);
} public DataTable GetContacts() { GetContactsRequest inValue = new GetContactsRequest(); GetContactsResponse retVal = ((IContactManager)(this)).GetContacts(inValue);
return retVal.GetContactsResult; }
当然,前提条件是我们需要修改AddContactRequest类以及GetContactsResponse,例如将AddContactRequest类的contacts成员由原来的AddContactsContacts类型修改为DataTable类型;将GetContactsResponse中的GetContactsResult成员由原来的GetContactsResponseGetContactsResult类型修改为DataTable类型。
自动生成的代理类非常复杂,实际上我们完全可以简化。首先将客户端的服务契约定义修改为与服务端服务契约完全一致的定义:
[ServiceContract()]
public interface IContactManager { [OperationContract]
void AddContact(Contact contact); [OperationContract]
void AddContacts(DataTable contacts);
[OperationContract] DataTable GetContacts(); }
然后修改代理类ContactManagerClient:
修改后运行的结果完全相同。
注意,DataRow类型是不能序列化的。
在WCF中,还可以使用DataTable和DataSet的类型安全的子类。书中也给出了相应的例子。然而,WCF的最佳实践则是避免使用DataTable和DataSet,以及使用DataTable和DataSet的类型安全的子类。书中阐释了原因:
“对于WCF的客户端与服务而言,虽然可以通过ADO.NET和Visual Studio工具使用DataSet、DataTable以及它们的类型安全的派生对象,但这种方式过于繁琐。而且,这些数据访问类型都是特定的.NET类型。在序列化时,它们生成的数据契约样式过于复杂,很难与其它平台进行交互。在服务契约中使用数据表或者数据集还存在一个缺陷,那就是它可能暴露内部的数据结构。同时,将来对数据库样式的修改会影响到客户端。虽然在应用程序内部可以传递数据表,但如果是跨越应用程序或公有的服务边界发送数据表,却并非一个好的主意。通常情况下,更好的做法是暴露数据的操作而非数据本身。”
最好的做法是将DataTable转换为数组类型。书中提供了DataTableHelper类,可以帮助将DataTable转换为数组类型。
泛型
非常遗憾,我们并不能在数据契约中定义泛型。但是,WCF使用了一个折中的办法,使得我们可以在服务端照常使用泛型,但在生成的数据契约定义时,泛型会被具体的类型所取代,重命名的格式为:
<原有名>Of<类型参数名><哈希值>
WCF还支持将自定义类型作为泛型参数。此外,还可以通过数据契约的Name属性为导出的数据契约指定不同的名字。例如,如下的服务端数据契约:
如下的服务端数据契约:
[DataContract]
class SomeClass {...} [DataContract(Name = "MyClass")] class MyClass<T> {...} [OperationContract]
void MyMethod(MyClass<SomeClass> obj);
导出的数据契约为:
[DataContract]
class SomeClass {...} [DataContract] class MyClass {...} [OperationContract]
void MyMethod(MyClass obj);
集合
WCF支持泛型集合、定制集合,但与传统的.NET编程不一样,WCF对集合的操作存在许多约束。对于这些约束,本书描述得非常清楚。本文不再赘述。
|