数据契约特性(DataContract Attribute)
在客户端与服务之间面向服务的交互Serializable还不够理想。与Serializable指明类型的所有成员都是可序列化的并作为组成类型数据样式的一部分相比;更好的方式是能够提供一种明确参与(Opt-In)的途径,只有那些契约的开发者明确包含的成员才应该放到数据契约中。
使用DataContract
[DataContract]
struct Contract
{
[DataMember]
//应将DataMember特性标记应用于属性,尽量不用在公有成员上
private string FirstName;
//没有标记[DataMember],则不会成为数据契约的一部分
private string LastName;
public string FirstName1 { get => FirstName; set => FirstName = value; }
public string LastName1 { get => LastName; set => LastName = value; }
}
注意
为什么WCF中的面向服务Attribute不用Serializable
- Serializable特性无法实现将类型作为WCF操作参数(类型的“服务”特性)与序列化能力之间的职责分离。
- Serializalbe特性不支持类型名和成员名的别名,也无法将一个新类型映射为预定义的数据契约。
- 由于Serializable特性可以直接操作成员字段,使得封装了字段访问的属性形同虚设。访问字段的最好办法是通过属性添加它们的值,而Serializable却破坏了属性的封装性。
- Serializable特性并没有直接支持版本控制(Versioning),因为格式器期望获取版本控制的所有信息。因而,它导致了版本控制的处理变得举步维艰。
导入数据契约
如果在契约操作中使用了数据契约,它就会被发布在服务的元数据中。当客户端使用诸如Visual Studio之类的工具导入数据契约的定义时,使用的是与它等效的定义,但并非必须是同一个数据契约。区别在于工具的功能,而不是发布的元数据。
使用Visual Studio导入数据契约效果
使用Visual Studio导入的定义会保留类或者结构原有的类型名,以及原类型的命名空间。
使用Visual Studio,即使原来的服务端类型并没有定义任何属性,在导入的定义中,仍然会以标记DataMember特性的属性来表示。
[DataContract]
struct Contact
{
[DataMember]
public string FirstName;
[DataMember]
public string LastName;
}
导入的客户端定义为
访问该字段的属性,同时为原来的字段名添加后缀Field
[DataContract]
public partial struct Contact
{
string FirstNameField;
string LastNameField;
[DataMember]
public string FirstName
{
get
{
return FirstNameField;
}
set
{
FirstNameField = value;
}
}
[DataMember]
public string LastName
{
get
{
return LastNameField;
}
set
{
LastNameField= value;
}
}
}
使用SvcUtil导入数据契约效果
只有数据契约会保留命名空间
namespace MyNamespace
{
[DataContract]
struct Contact
{...}
[ServiceContract]
interface IContractManager
{
[OperationContract]
void AddContract(Contact contact);
[OperationContract]
Contact[] GetContacts();
}
}
SvcUtil导入的定义则发布为
namespace MyNamespace
{
[DataContract]
struct Contract
{...}
}
[ServiceContract]
interface IContractManager
{
[OperationContract]
void AddContract(Contract contract);
[OperationContract]
Contract[] GetContracts();
}
可以通过DataContract特性的Namespace属性设置命名空间
namespace MyNamespace
{
[DataContract(Namespace = "MyOtherNamespace")]
struct Contract
{...}
}
SvcUtil导入的定义则发布为
namespace MyOtherNamespace
{
[DataContract]
struct Contract
{...}
}
注意
DataMember
当DataMember特性应用到属性上时(不管是服务还是客户端),该属性必须具有get和set访问器。如果没有,在调用时就会抛出InvalidDataContractException异常。因为当属性自身就是数据成员时,WCF会在序列化和反序列化时使用该属性,使开发者能够将定制逻辑应用到属性中。
数据契约与Serializable特性
服务仍然能够使用只标记了Serializable特性的类型:
[Serializable]
struct Contact
{
string m_FirstName;
public string LastName;
}
导入的定义会使用DataContract特性
[DataContract]
public partial struct Contact
{
string LastNameField;
string m_FirstNameField;
[DataMember(...)]
public string LastName
{
... //访问LastNameField
}
[DataMember(...)]
public string m_FirstName
{
... //访问m_FirstNameField
}
}
客户端同样可以在它的数据契约中使用Serializable特性
注意
传统的格式器不能序列化只标记了DataContract特性的类型
传统的格式器不能序列化只标记了DataContract特性的类型。要序列化这样的类型,必须同时应用DataContract特性和Serializable特性。对于该类型生成的数据契约,效果与只应用了DataContract特性相同,同时,我们仍然需要为需要序列化的成员添加DataMember特性。
XML序列化
.NET还提供了另外一种序列化机制:XML序列化,它使用了一系列专门的特性。如果我们正在处理的数据类型需要显式地控制XML序列化,那么就应该将XmlSerializerFormateAttribute特性应用到契约定义中单独的操作上,以指导WCF在运行时使用XML序列化。如果契约的所有操作都需要采用这种序列化形式,我们就可以使用SvcUtil(如第1章所述)的/serializer:XmlSerializer开关,指导它自动地将XmlSerializerFormat特性应用到所有导入的契约中的所有操作上。使用这一开关时需要谨慎,因为它会影响所有的数据契约,包括那些不需要显式控制XML序列化的数据契约。
推断数据契约
如果编组类型是公共类型,且未曾标记DataContract特性,WCF就会自动推断,认为DataContract特性被应用到该类型上,且它的所有公有成员(字段或属性)均被应用了DataMember特性。
定义如下服务契约
public struct Contact
{
public string FirstName
{ get; set; }
public string LastName;
internal string PhoneNumber;
string Address;
}
[ServiceContract]
interface IContactManager
{
[OperationContract]
void AddContact(Contact contact);
}
WCF推断的数据契约
public class Contact
{
[DataMember]
public string FirstName
{ get; set; }
[DataMember]
public string LastName;
}
如果类型已经包含了DataMember特性(但未包含DataContract特性),则DataMember特性就会被忽略,就像它们不存在一般。如果类型包含了DataContract特性,就不会推断数据契约。
不要用推断数据契约
依赖于数据契约的推断是一种草率的破解方法,它与WCF的大多数内容背道而
驰。既然WCF无法从仅有的接口定义中推断出服务契约,或者默认允许事务或可靠性,它就不应该推断数据契约。面向服务在很大程度上偏向于默认“否决”的方式(安全是一个例外),正因为如此,它需要最大限度的封装与解耦。对数据契约显式地使用DataContract特性,就使得你可以利用数据契约的功能,例如版本控制。
组合数据契约
定义数据契约时, 对于那些本身就是数据契约的成员, 我们仍然可以为它们标记
DataMember特性,如下。
[DataContract]
class Address
{
[DataMember]
public string Street;
[DataMember]
public string City;
[DataMember]
public string State;
[DataMember]
public string Zip;
}
[DataContract]
struct Contact
{
[DataMember]
public string FirstName;
[DataMember]
public string LastName;
[DataMember]
public Address Address;
}
当发布一个合成的数据契约时,它所包含的所有数据契约都会被发布。
数据契约事件
概述
.NET为可序列化类型引入了对序列化事件的支持,WCF沿袭了这一技术,对数据契约提供了同样的支持。执行序列化和反序列化时,WCF会调用数据契约的指定方法。WCF一共定义了四个序列化与反序列化事件。
- serializing事件发生在序列化之前
- serialized事件则发生在序列化之后
- deserializing事件发生在反序列化之前
- deserialized事件则发生在反序列化之后
如下
[DataContract]
class MyDataContract
{
[OnSerializing]
void OnSerializing(StreamingContext context)
{...}
[OnSerialized]
void OnSerialized(StreamingContext context)
{...}
[OnDeserializing]
void OnDeserializing(StreamingContext context)
{...}
[OnDeserialized]
void OnDeserialized(StreamingContext context)
{...}
//数据成员
}
每个序列化事件的处理方法都必须遵循如下的方法签名:
void <Method Name>(StreamingContext context);
序列化事件顺序图
反序列化事件顺序图
注意:
1. 为了调用deserializing事件的处理方法,WCF必须首先创建一个对象;但是它不会调用数据契约类的默认构造函数。
2. WCF禁止将相同的序列化事件特性应用到数据契约类型的多个方法上。遗憾的是,这样就无疑将partial类型排除在外了,因为partial类型的每一部分都有可能处理自己的序列化事件。
使用Deserializing事件
在反序列化期间,没有调用构造函数,deserialization事件的处理方法在逻辑上就是反序列化的构造函数。这样做的目的在于它能够在反序列化之前执行某些定制步骤。通常,用于初始化的类成员并不需要被标记为数据成员。那些被标记为数据成员的类成员,在初始化时设置的任何值都是无效的,因为WCF会在反序列化期间根据从消息获得的值,对这些成员进行重新设置。此外,反序列化事件的处理方法还能够设置特定的环境变量
(如线程本地存储),执行诊断操作,或者通知某些全局的同步事件。如果提供了这样的deserializing事件处理方法,则可以让默认的构造函数和事件处理器调用同一个辅助方
法,这样在使用常规.NET方式实例化类型时,完全可以执行相同的步骤,并在一个单独的
地方维护这些代码:
[DataContract]
class MyClass
{
public MyClass()
{
OnDeserializing();
}
[OnDeserializing]
void OnDeserializing(StreamingContext context)
{
OnDeserializing();
}
void OnDeserializing()
{...}
}
使用Deserialized事件
在使用已经反序列化的值时,deserialized事件允许初始化数据契约或者重新声明非数
据成员。例子演示了deserialized事件的功能。它使用事件初始化数据库连接。如果没
有事件的支持,数据契约是无法实现这样的功能的,因为构造函数永远都不会被调用,则
connection对象将为空。
使用deserialized事件初始化不可序列化的资源
[DataContract]
class MyDataContract
{
IDbConnection m_Connection;
[OnDeserialized]
void OnDeserialized(StreamingContext context)
{
m_Connection = new SqlConnection(...);
}
/* 数据成员*/
}
共享数据契约(多个服务契约共享一个数据契约)
在Visual Studio 2008添加一个服务引用时,你必须为每个服务引用提供唯一的新命名空间。导入的类型会定义在这个新的命名空间中。如果为共享了相同数据契约的两个不同服务添加引用,就会出现问题,因为你得到了两个不同的类型,在两个不同的命名空间,表示的却是相同的数据契约。然而,默认情况下,如果被客户端引用的任意一个程序集包含的数据契约,与已经暴露在引用服务元数据的数据契约类型匹配,Visual Studio 2008就不会再次导入。需要再次强调的是,已有的数据契约引用必须是在另一个引用程序集中,而不是在客户端项目自身。这一限制会在未来的Visual Studio版本中提供,而目前最方便的弥补措施与最佳实践则为:将所有共享的数据契约分解到指定的类库中,并让所有的客户端引用该程序集。然后,通过服务引用的高级设置对话框(参见图1-10),可以控制和配置引用程序集(如果存在)与有关的共享数据契约进行协调。“Reuse types in referenced assemblies”检查框默认是被选中的,但如果你需要也可以关闭这一功能。顾名思义,你只能共享数据契约,却不能共享服务契约。使用里面的单选按钮,可以让Visual Studio 2008跨所有的引用程序集重用数据契约,或者通过选择列表项限制对特定程序集的共享。