WCF Data Contract之集合类型
LazyBee
在.Net中将实现了IEnumerable接口的所有类型(包括数组和泛型)都称之为集合类型。把其中实现了IDictionary接口或泛型IDictionary接口的集合类型称为字典集合,剩下的其他集合类型为列表集合。
1集合的数据契约(协定)缺省名称
缺省情况下,WCF框架对集合类型是内建支持的,也就说你不需要应用任何属性,就可以将集合应用在数据契约(协定)中,但前提是集合中的元素必须是应用了DataContractAttribute属性或者是可序列化的类型。这时,数据契约(协定)名称和命名空间就依赖集合中包含的元素的类型的名称和命名空间了,它们不受集合类型本身的名称和命名空间的影响。
缺省集合类型数据契约(协定)的格式是(不包括“+”):
列表集合:名称:ArrayOf+集合中包含的元素类型
循环元素名称:集合中包含的元素类型
字典集合:名称:ArrayOfKeyValueOf+集合中Key的类型+集合中包含的对象类型
循环元素名称:KeyValueOf+集合中Key的类型+集合中包含的对象类型
例如:
MyCollection1 : IList<int>{…}的数据契约名称就是:ArrayOfint
MyCollection2 : ICollection<int>{…}的数据契约名称就是:ArrayOfint
MyDictionary1 : Dictionary<int, int>{…}的数据契约名称就是:ArrayOfKeyValueOfintint
MyCollection3 : ArrayList{…}的数据契约名称就是:ArrayOfanyType
MyDictionary2 : Dictionary<int, object>{…}的数据契约名称就是:ArrayOfKeyValueOfintanyType
注意:如果是object的话,使用的是anyType,因为在Schema中所有类型的基类是anyType.
如果集合是应用于某个数据契约类型中时,那么它的名称将是字段名称,如下面Customer的定义以及序列化后的表示:
[DataContract]
public class Customer
{
[DataMember]
public List<string> addresses = new List<string> {"
[DataMember]
public Dictionary<int, object> telephones = new Dictionary<int, object> {
{ 1, "010-82371234" },
{ 2, "021-56781234" } };
}
<Customer xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://schemas.datacontract.org/2004/07/WCFTestSerializer">
<addresses xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<d2p1:string>
<d2p1:string>
</addresses>
<telephones
xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<d2p1:KeyValueOfintanyType>
<d2p1:Key>1</d2p1:Key>
<d2p1:Value xmlns:d4p1="http://www.w3.org/2001/XMLSchema" i:type="d4p1:string">010-82371234</d2p1:Value>
</d2p1:KeyValueOfintanyType>
<d2p1:KeyValueOfintanyType>
<d2p1:Key>2</d2p1:Key>
<d2p1:Value xmlns:d4p1="http://www.w3.org/2001/XMLSchema" i:type="d4p1:string">021-56781234</d2p1:Value>
</d2p1:KeyValueOfintanyType>
</telephones>
</Customer>
2集合的契约等价
在WCF中应用了CollectionDataContractAttribute属性的集合称之为定制数据契约集合,否则为非定制数据契约集合。不管是非定制数据契约集合还是定制数据契约集合,只要它们的数据契约名称和循环元素的名称都相同(如果是字典集合其Key和Value也要是相同的),我们就说它们是等价的。由于非定制数据契约集合的数据契约以及循环元素的名称由集合中的类型决定,所以非定制数据契约集合的数据契约等价遵守以下几个规则(关于数据契约等价的其他详细信息请参见我以前的文章:WCF Data Contract之契约等价):
2.1相同类型的列表集合被认为具有相同的数据契约(协定)例如:List<int>和int[]是等价的。
2.2具有相同相同键和值类型的所有字典集合也被视为具有相同的数据契约(协定)
2.3接口和实现该接口的具体集合类的数据契约是等价的。例如IList<int>和List<int>等价。
2.4非泛型集合与Object类型的泛型集合的数据契约等价。例如:List<Object>与ArrayList等价,ArrayList与Object[]也是等价的。
这就是为什么上面的示例中,实现IList<int>和ICollection<int>的集合类型的数据契约名称都是ArrayOfint的原因。例如下面两个数据契约是等价的:
[DataContract(Name="Customer")] public class Customer1
{
[DataMember] public string customerName;
[DataMember] public Collection<string> addresses;
[DataMember] public string[] telephones;
}
[DataContract(Name="Customer")] public class Customer2
{
[DataMember] public string customerName;
[DataMember] public ICollection<string> addresses;
[DataMember] public List<string> telephones;
}
注意:Collection<string>和ICollection<string>是等价的,同时string[]和List<string>也是等价的。
所以,针对Customer2的定义,我们可以将实现ICollection<string>接口的任何集合类的实例赋给addresses。同样,我们也可以利用在数据契约中定义集合接口的机制来将WCF不支持的集合类型(如:ReadOnlyCollection,具体见本文中WCF中对集合类型的要求限制一节)来应用到我们的WCF应用中。也就是说我们可以将ReadOnlyCollection的实例赋给Customer2的addresses.
3 集合与KnowType
集合类型和非集合类型在多态增加KnowType类型方面是不同的,关于非集合方面增加KnowType的详细情况请我以前的文章:WCF Data Contract之KnowType。集合类型在增加KnowType类型遵守以下规则:
3.1集合类型是以多态方式来代替其他集合或集合接口的,您不需要将这样的集合类型添加到KnowType类型中。例如上例中Customer2类中的addresses,你可以将List<string>的实例赋值给它,而不需要将List<string>增加到KnowType类型列表中。
3.2当您以多态方式使用集合来代替非集合类型时,则需要将它们添加到已知类型。 例如,如果您声明一个 Object 类型的数据成员并将其用于发送 ArrayList 的一个实例,则需要将 ArrayList 添加到已知类型中。
3.3 等价的集合只能应用KnowTypeAttribute属性来将其增加到KnowType列表中一次。例如:不能将ArrayList和Object[]都添加到相同类的KnowType列表中
4 定制集合的数据契约(协定)
我们可以使用CollectionDataContractAttribute的下列属性来指定集合的数据契约的相关名称及命名空间:
4.1 Name属性来指定集合数据契约的名称(如果没有使用此属性,将使用集合类型的名称)
4.2 Namespace属性来指定其命名空间
4.3 ItemName 属性来指定循环元素的名称
4.4 针对字典集合还可以用KeyName和ValueName来指定键和值的名称
例如我们将第一节的例子更改成如下所示:
[CollectionDataContract(Name = " telephones", ItemName = "telephone",
KeyName = "Index", ValueName = "Number")]
public class MyDictionary : Dictionary<int, object>
{
public new Dictionary<int,object>.Enumerator GetEnumerator()
{
Dictionary<int, object> innerObject = new Dictionary<int, object> {
{ 1, "010-82371234" },
{ 2, "021-56781234" } };
return innerObject.GetEnumerator();
}
}
此类将被序列化成:
<telephones xmlns:i=http://www.w3.org/2001/XMLSchema-instance xmlns="http://schemas.datacontract.org/2004/07/WCFTestSerializer">
<telephone>
<Index>1</Index>
<Number xmlns:d4p1=http://www.w3.org/2001/XMLSchema i:type="d4p1:string">010-82371234</Number>
</telephone>
<telephone>
<Index>2</Index>
<Number xmlns:d4p1=http://www.w3.org/2001/XMLSchema i:type="d4p1:string">021-56781234</Number>
</telephone>
</telephones>
对于定制数据契约的集合类型来说,前面所述的非定制数据契约的集合等价规则将失效。所以要尽量避免使用CollectionDataContractAttribute。
5 集合的反序列化
缺省情况下,使用Svcutil.exe生成客户端代理时,列表集合将反序列化成数组,字典集合将反序列化成Dictionary泛型。我们也可以通过/collectionType 命令行开关(简写形式是 /ct)来指定我们希望反序列化的集合类型(请记住,您还必须使用 /reference 开关(简写形式是 /r)指定引用的集合类型的程序集)。如果该类型是泛型,则必须在类型后面跟有反引号和泛型参数的数目。例如前面的例子中的Customer1类可以通过下面的命令在客户端使用List泛型:SvcUtil http://localhost:8000/
/r:C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.dll
/ct:System.Collections.Generic.List`1
6 DataContractAttribute和CollectionContractAttribute
对于集合而言,WCF框架将隐含地自动的为集合类型应用CollectionDataContractAttribute属性的,这就是为什么你不需要为集合应用任何属性就可以在数据契约中使用的原因。但要注意:
1 如果我们新建的集合类型是继承已有的集合类型如List<string>,那么我们就不能对新建的集合类型应用DataContractAttribute,否则运行时会抛出InvalidDataContractException,但你可以应用CollectionDataContractAttribute来定制集合类型的数据契约。例如
[DataContract]public class MyList:List<string>{…}的集合定义将抛出异常。
2 如果我们新建的集合类型是实现了集合接口例如IList<int>,IDictionary<int,int>的话,我们可以对此类型应用DataContractAttribute属性,这样的话此类型将作为普通的数据契约类型,而不是将其作为集合类型来处理。也就是WCF框架将只序列化其中应用了DataMemberAttribute属性的成员。当然你也可以不应用任何属性来让系统缺省作为集合类型来处理。(你也可以使用CollectionDataContractAttribute来定制数据契约)
3 针对应用CollectionDataContractAttribute属性或者缺省不应用任何属性的集合类型,如果其内部有应用了DataMemberAttribute的属性或字段,在序列化时系统将忽略。
7 WCF中对集合类型的要求限制
不是所有的集合类型都可以在WCF中使用,只有满足以下要求才可以使用:
2.1 该集合类型有一个缺省的构造函数
2.2 该集合类型有一个名为Add的方法
这是因为在反序列化集合类型时,WCF框架首先调用该集合类型的无参数的构造函数,然后通过非静态的Add方法来将循环元素增加到集合中。所以以上限制主要是针对反序列化而设定的。
8 集合中的一些高级规则
8.1 WCF框架在序列化时支持集合的集合,也支持数组的数组(交错数组),但不支持多唯数组。
8.2 字节数组和 XmlNode 数组是特殊的数组类型,将被视为基元,而不是集合。 序列化字节数组会产生单个包含一个 Base64 编码数据块的 XML 元素,而不是为每个字节都生成一个单独的元素。(笔者认为这是为了性能的考虑才这么处理的。)
8.3 如果集合类型实现了IXMLSerializable接口,假设类型为MyType:IList<string>,IXMLSerializable{…},WCF框架将根据在数据契约中声明的类型来进行序列化,如果声明的是集(接口)如IList<string>,那么该类型将被认为是列表集合来序列化,如果声明的是IXMLSerializable,那么将按照IXMLSerializable来进行序列化,当然需要将该类型加到KnowType类型列表中。如果声明的是该类型本身(如MyType),那么将按照IXMLSerializable的规则来进行序列化。
8.4 在对集合进行序列化时,将调用集合类的GetEnumerator 方法来得到集合的内容,在反序列化时将首先调用该集合类型的无参数的构造函数,然后通过非静态的Add方法来将循环元素增加到集合中。(注:虽然这与大家在MSDN的帮助文档中看到的不同,认为字典集合将调用get_Keys和get_Values,以及IList将调用索引器,但笔者使用VS2008验证时没有得到以上方法被调用的结论,所以笔者认为是MSDN文档滞后或有误,如果各位看官能得到和MSDN吻合的结论麻烦告诉一声。)
8.5 如果集合类型同时应用了Serialized属性或实现了ISerializable接口,WCF框架将忽略它们;但是如果集合类型不满足集合类型要求(例如缺少Add)方法,那么将按照Serialized或ISerializable来处理;但如果你对该集合同时应用了CollectionDataContract属性而且又不满足集合要求,那么将抛出InvalidDataContractException,而不是按照Serialized或ISerializable来处理。
8.6 不能向实现了IXmlSerializable接口的类型使用CollectionDataContractAttribute属性,否则会抛出InvalidDataContractException.向非集合应用CollectionDataContractAttribute属性以及非字典集合指定KeyName或者ValueName属性也都将抛出此异常。