数据协定代理是基于数据协定模型构建的高级功能。 在用户想要更改类型序列化、反序列化或投影到元数据的方式的情况下,此功能用于类型自定义和替换。 某些可能使用代理项的情况包括:未为类型指定数据协定、字段和属性未标记 DataMemberAttribute 属性,或者用户希望动态创建架构变体。
序列化和反序列化是在使用 DataContractSerializer 将 .NET Framework 转换为合适的格式(如 XML)时,借助于数据协定代理项来完成的。 在生成元数据表示形式(如 XML 架构文档 (XSD))时,也可以使用数据协定代理项来修改为类型导出的元数据。 导入后,代码是从元数据创建的,代理项也可用于自定义生成的代码。
代理项的工作方式
代理项的工作原理是将一种类型(“原始”类型)映射到另一种类型(“代理项”类型)。 以下示例显示了Inventory
原始类型和InventorySurrogated
新的代理类型。 类型 Inventory
不可序列化,但类型 InventorySurrogated
为:
public class Inventory
{
public int pencils;
public int pens;
public int paper;
}
由于尚未为此类定义数据协定,因此将该类转换为具有数据协定的代理项类。 代理类显示在以下示例中:
[DataContract(Name = "Inventory")]
public class InventorySurrogated
{
[DataMember]
public int numpencils;
[DataMember]
public int numpaper;
[DataMember]
private int numpens;
public int pens
{
get { return numpens; }
set { numpens = value; }
}
}
实现 IDataContractSurrogate
若要使用数据协定代理项,请实现 IDataContractSurrogate 接口。
下面概述了 IDataContractSurrogate 的每种方法及其可能的实现。
GetDataContractType
该方法 GetDataContractType 将一种类型映射到另一种类型。 序列化、反序列化、导入和导出需要此方法。
第一个任务是定义哪些类型将被映射到其他类型。 例如:
public Type GetDataContractType(Type type)
{
Console.WriteLine("GetDataContractType");
if (typeof(Inventory).IsAssignableFrom(type))
{
return typeof(InventorySurrogated);
}
return type;
}
在序列化时,此方法返回的映射随后用于通过调用 GetObjectToSerialize 该方法将原始实例转换为代理实例。
在反序列化时,序列化程序将使用 GetDataContractType 方法所返回的映射来反序列化为代理项类型的实例。 随后调用 GetDeserializedObject 将代理实例转换为原始类型的实例。
在导出时,将反射 GetDataContractType 方法所返回的代理项类型以获得要用于生成元数据的数据协定。
在导入时,原始类型会更改为代理项类型,代理项类型将进行反射以获得要用于各种目的(如引用支持)的数据协定。
参数 Type 是正在序列化、反序列化、导入或导出的对象的类型。 如果代理项未处理类型,该方法 GetDataContractType 必须返回输入类型。 否则将返回相应的代理项类型。 如果存在多个代理项类型,则可以在此方法中定义许多映射。
GetDataContractType 方法不是针对内置的数据协定基元(如 Int32 或 String)调用的。 对于其他类型的(如数组、用户定义的类型和其他数据结构),将为每个类型调用此方法。
在前面的示例中,该方法检查参数是否type
Inventory
可比较。 如果是这样,该方法将它映射到 InventorySurrogated
。 每当调用序列化、反序列化、导入架构或导出架构时,首先调用此函数以确定类型之间的映射。
GetObjectToSerialize 方法
该方法 GetObjectToSerialize 将原始类型实例转换为代理类型实例。 序列化需要该方法。
下一步是通过实现 GetObjectToSerialize 该方法来定义物理数据从原始实例映射到代理项的方式。 例如:
public object GetObjectToSerialize(object obj, Type targetType)
{
Console.WriteLine("GetObjectToSerialize");
if (obj is Inventory)
{
InventorySurrogated isur = new InventorySurrogated();
isur.numpaper = ((Inventory)obj).paper;
isur.numpencils = ((Inventory)obj).pencils;
isur.pens = ((Inventory)obj).pens;
return isur;
}
return obj;
}
序列化对象时会调用GetObjectToSerialize方法。 此方法将数据从原始类型传输到代理类型的字段。 字段可以直接映射到代理字段,或者原始数据的处理结果可以存储在代理字段中。 一些可能的用途包括:直接映射字段、对要存储在代理字段中的数据执行作,或将原始类型的 XML 存储在代理项字段中。
该 targetType
参数引用成员的声明类型。 此参数是由 GetDataContractType 方法返回的替代类型。 序列化程序不强制将返回的对象分配给此类型。 参数 obj
是要序列化的对象,并在必要时转换为其代理项。 如果代理项未处理该对象,此方法必须返回输入对象。 否则,将返回新的代理项对象。 如果对象为 null,则不调用代理项。 可以在此方法中定义不同实例的许多代理项映射。
创建对象 DataContractSerializer时,可以指示它保留对象引用。 (有关详细信息,请参阅 序列化和反序列化。)为此, preserveObjectReferences
将构造函数中的参数设置为 true
. 在这种情况下,代理项只为对象调用一次,因为所有后续序列化只是将引用写入流中。 如果 preserveObjectReferences
设置为 false
,则每次遇到实例时都会调用代理项。
如果序列化的实例类型不同于声明的类型,则会将类型信息(例如,xsi:type
)写入流中,以便在另一端可以对实例进行反序列化。 无论该对象是否为代理项对象,都会发生此过程。
上述示例将 Inventory
实例的数据转换为 InventorySurrogated
实例的数据。 它会检查对象的类型,并执行必要的操作以转换为代理类型。 在这种情况下,类的 Inventory
字段直接复制到 InventorySurrogated
类字段。
GetDeserializedObject 方法
该方法 GetDeserializedObject 将代理项类型实例转换为原始类型实例。 它是反序列化所必需的。
下一个任务是定义将物理数据从代理项实例映射到原始实例的方式。 例如:
public object GetDeserializedObject(object obj, Type targetType)
{
Console.WriteLine("GetDeserializedObject");
if (obj is InventorySurrogated)
{
Inventory invent = new Inventory();
invent.pens = ((InventorySurrogated)obj).pens;
invent.pencils = ((InventorySurrogated)obj).numpencils;
invent.paper = ((InventorySurrogated)obj).numpaper;
return invent;
}
return obj;
}
此方法仅在对象的反序列化期间调用。 此方法提供了用来从代理项类型反序列化到其原始类型的反向数据映射。 与 GetObjectToSerialize
该方法类似,一些可能的用途可能是直接交换字段数据、对数据执行作以及存储 XML 数据。 反序列化时,由于数据转换过程中可能的变动,您可能无法总是从原始数据中获得确切的数据值。
该 targetType
参数引用成员的声明类型。 此参数是由 GetDataContractType
方法返回的替代类型。 该 obj
参数引用已反序列化的对象。 如果对象是代理项,则可以将其转换回其原始类型。 如果代理项未处理该对象,此方法将返回输入对象。 否则,反序列化对象将在转换完成后返回。 如果存在多个代理项类型,则可以通过指示每种类型及其转换来提供从代理项到主要类型的数据转换。
返回对象时,内部对象表使用此代理项返回的对象进行更新。 对实例的任何后续引用都将从对象表中获取代理实例。
前面的示例将类型 InventorySurrogated
的对象转换回初始类型 Inventory
。 在这种情况下,数据直接从 InventorySurrogated
传输回到其在 Inventory
中的相应字段。 由于没有数据操作,因此每个成员字段将包含与序列化前相同的值。
GetCustomDataToExport 方法
导出架构时,该方法 GetCustomDataToExport 是可选的。 它用于将其他数据或提示插入导出的架构中。 可以在成员级别或类型级别插入其他数据。 例如:
public object GetCustomDataToExport(System.Reflection.MemberInfo memberInfo, Type dataContractType)
{
Console.WriteLine("GetCustomDataToExport(Member)");
System.Reflection.FieldInfo fieldInfo = (System.Reflection.FieldInfo)memberInfo;
if (fieldInfo.IsPublic)
{
return "public";
}
else
{
return "private";
}
}
此方法(连同两个重载)允许在成员级别或类型级别将额外的信息包括到元数据中。 可以包括关于成员是公共还是私有的提示,并在整个架构导出和导入过程中保留这些注释。 如果没有此方法,此信息将丢失。 此方法不会导致插入或删除成员或类型,而是在这些级别之一向架构添加其他数据。
此方法是重载方法,可以采用 Type
(clrtype
参数)或 MemberInfo(memberInfo
参数)。 第二个参数始终是一个 Type
(dataContractType
参数)。 此方法针对代理类型 dataContractType
的每个成员及其类型进行调用。
这些重载之一必须返回 null
或可序列化的对象。 非空对象将序列化为注释,并包含在导出的架构中。 对于 Type
重载,导出到架构的每个类型都会通过第一个参数,连同作为 dataContractType
参数的代理项类型一起发送到此方法。 对于 MemberInfo
重载,导出到架构的每个成员的成员信息都会连同第二个参数中的代理项类型,作为 memberInfo
参数来发送。
GetCustomDataToExport 方法 (Type, Type)
在架构导出期间,此 IDataContractSurrogate.GetCustomDataToExport(Type, Type) 方法对每个类型定义进行调用。 此方法在导出时将信息添加到架构中的类型。 定义的每种类型都会发送到此方法,以确定架构中是否需要包含任何其他数据。
GetCustomDataToExport 方法 (MemberInfo, Type)
在导出过程中,将调用 IDataContractSurrogate.GetCustomDataToExport(MemberInfo, Type) 函数以处理每个导出类型中的成员。 使用此函数,可以针对在导出时将要包括在架构中的成员自定义任何注释。 类中每个成员的信息将发送到此方法,以检查是否需要在架构中添加任何其他数据。
上面的示例通过每个代理项成员的 dataContractType
进行搜索。 然后,它将为每个字段返回相应的访问修饰符。 如果没有此自定义项,则访问修饰符的默认值是公共的。 因此,无论其实际访问限制是什么,所有成员都将在使用导出的架构生成的代码中定义为公共成员。 如果不使用此实现,即使该成员在代理项中定义为私有,该成员 numpens
也会在导出的架构中公开。 通过使用此方法,可以将所导出架构中的访问修饰符生成为 private(私有)。
GetReferencedTypeOnImport 方法
此方法将 Type 代理项映射到原始类型。 此方法对于架构导入是可选的。
创建导入架构并为其生成代码的代理项时,下一个任务是将代理项实例的类型定义为其原始类型。
如果生成的代码需要引用现有用户类型,则通过实现 GetReferencedTypeOnImport 该方法来完成此作。
导入架构时,会为每个类型声明调用此方法,以便将代理项数据协定映射到类型。 字符串参数 typeName
并 typeNamespace
定义代理类型的名称和命名空间。 返回值 GetReferencedTypeOnImport 用于确定是否需要生成新类型。 此方法必须返回有效类型或 null。 对于有效类型,返回的类型将用作生成的代码中的引用类型。 如果返回 null,则不会引用任何类型,并且必须创建一个新类型。 如果存在多个代理,可以将每个代理类型映射回其初始类型。
参数 customData
是最初从 GetCustomDataToExport中返回的对象。 代理作者希望将额外的数据/提示插入到元数据中以便在导入期间生成代码时,会使用此 customData
。
ProcessImportedType 方法
该方法 ProcessImportedType 自定义从架构导入创建的任何类型。 此方法是可选的。
导入架构时,此方法允许自定义任何导入的类型和编译信息。 例如:
public System.CodeDom.CodeTypeDeclaration ProcessImportedType(System.CodeDom.CodeTypeDeclaration typeDeclaration, System.CodeDom.CodeCompileUnit compileUnit)
{
Console.WriteLine("ProcessImportedType");
foreach (CodeTypeMember member in typeDeclaration.Members)
{
object memberCustomData = member.UserData[typeof(IDataContractSurrogate)];
if (memberCustomData != null
&& memberCustomData is string
&& ((string)memberCustomData == "private"))
{
member.Attributes = ((member.Attributes & ~MemberAttributes.AccessMask) | MemberAttributes.Private);
}
}
return typeDeclaration;
}
在导入期间,会针对生成的每种类型调用此方法。 更改指定的 CodeTypeDeclaration 或修改 CodeCompileUnit。 这包括更改CodeTypeDeclaration
的名称、成员、属性以及其他许多特性。 通过处理 CodeCompileUnit
,可以修改指令、命名空间、引用的程序集和其他几个方面。
该 CodeTypeDeclaration
参数包含代码 DOM 类型声明。 该 CodeCompileUnit
参数允许修改以处理代码。 返回 null
的结果会导致类型声明被丢弃。 相反,当返回一个 CodeTypeDeclaration
时,修改将被保留。
如果在元数据导出期间插入自定义数据,则需要在导入过程中向用户提供该数据,以便使用它。 此自定义数据可用于编程模型提示或其他注释。 每个 CodeTypeDeclaration
和 CodeTypeMember 实例都包含作为 UserData 属性的自定义数据,并转化为 IDataContractSurrogate
类型。
上面的示例对导入的架构执行一些更改。 该代码通过使用替代对象来保留原始类型的私有成员。 导入架构时的默认访问修饰符为 public
。 因此,除非修改,否则代理项架构的所有成员都将是公共的,如此示例中所示。 在导出期间,自定义数据将插入到有关哪些成员是私有的元数据中。 该示例查询自定义数据,检查访问修饰符是否为私有,然后通过设置属性将相应的成员修改为私有。 如果没有此自定义项,成员 numpens
将定义为公共成员而不是专用成员。
GetKnownCustomDataTypes 方法
此方法从架构中获取定义的自定义数据类型。 此方法对于架构导入是可选的。
该方法在架构导出和导入的开头调用。 该方法返回导出或导入的架构中使用的自定义数据类型。 该方法将传递一个 Collection<T> ( customDataTypes
参数),该参数是类型的集合。 该方法应向此集合添加其他已知类型。 需要使用已知的自定义数据类型来启用通过DataContractSerializer进行自定义数据的序列化和反序列化。 有关详细信息,请参阅 数据协定已知类型。
实现代理项
若要在 WCF 中使用数据协定代理项,必须遵循一些特殊过程。
使用代理项进行序列化和反序列化
使用 DataContractSerializer 可以通过代理项对数据执行序列化和反序列化。 DataContractSerializer由DataContractSerializerOperationBehavior创建。 还必须指定代理项。
实现序列化和反序列化
为你的服务创建 ServiceHost 实例。 有关完整说明,请参阅 基本 WCF 编程。
对于指定服务主机的每个 ServiceEndpoint,查找它的 OperationDescription。
通过操作行为进行搜索,确定是否找到了 DataContractSerializerOperationBehavior 的实例。
如果找到了一个 DataContractSerializerOperationBehavior,请将它的 DataContractSurrogate 属性设置为该代理项的新实例。 如果未找到DataContractSerializerOperationBehavior,则创建一个新实例,并将新行为的DataContractSurrogate成员设置为替代项的新实例。
最后,将这个新行为添加到当前操作行为,如以下示例所示:
using (ServiceHost serviceHost = new ServiceHost(typeof(InventoryCheck))) foreach (ServiceEndpoint ep in serviceHost.Description.Endpoints) { foreach (OperationDescription op in ep.Contract.Operations) { DataContractSerializerOperationBehavior dataContractBehavior = op.Behaviors.Find<DataContractSerializerOperationBehavior>() as DataContractSerializerOperationBehavior; if (dataContractBehavior != null) { dataContractBehavior.DataContractSurrogate = new InventorySurrogated(); } else { dataContractBehavior = new DataContractSerializerOperationBehavior(op); dataContractBehavior.DataContractSurrogate = new InventorySurrogated(); op.Behaviors.Add(dataContractBehavior); } } }
使用代理项来导入元数据
导入 WSDL 和 XSD 等元数据以生成客户端代码时,需要将代理项添加到负责从 XSD 架构 XsdDataContractImporter生成代码的组件。 为此,请直接修改用于导入元数据的 WsdlImporter。
实现用来导入元数据的代理项
使用 WsdlImporter 类导入元数据。
使用TryGetValue方法检查XsdDataContractImporter是否已定义。
如果TryGetValue方法返回
false
,请创建一个新的XsdDataContractImporter,并将其Options属性设置为ImportOptions类的新实例。 否则,请使用由out
方法的 TryGetValue 参数所返回的导入程序。如果没有为 XsdDataContractImporter 定义 ImportOptions,请将该属性设置为 ImportOptions 类的新实例。
对于 DataContractSurrogate 的 ImportOptions,请将 XsdDataContractImporter 属性设置为该代理项的新实例。
将 XsdDataContractImporter 添加到 WsdlImporter 的 State 属性返回的集合中(继承自 MetadataExporter 类)。
使用 ImportAllContracts 的 WsdlImporter 方法导入架构中所有的数据协定。 在最后一步中,通过调用代理从加载的模式生成代码。
MetadataExchangeClient mexClient = new MetadataExchangeClient(metadataAddress); mexClient.ResolveMetadataReferences = true; MetadataSet metaDocs = mexClient.GetMetadata(); WsdlImporter importer = new WsdlImporter(metaDocs); object dataContractImporter; XsdDataContractImporter xsdInventoryImporter; if (!importer.State.TryGetValue(typeof(XsdDataContractImporter), out dataContractImporter)) xsdInventoryImporter = new XsdDataContractImporter(); xsdInventoryImporter = (XsdDataContractImporter)dataContractImporter; xsdInventoryImporter.Options ??= new ImportOptions(); xsdInventoryImporter.Options.DataContractSurrogate = new InventorySurrogated(); importer.State.Add(typeof(XsdDataContractImporter), xsdInventoryImporter); Collection<ContractDescription> contracts = importer.ImportAllContracts();
使用代理项来导出元数据
默认情况下,从 WCF 导出服务的元数据时,需要生成 WSDL 和 XSD 架构。 该代理项需要添加到负责为数据协定类型生成 XSD 架构的组件 XsdDataContractExporter 中。 为此,可以使用实现 IWsdlExportExtension 的行为来修改 WsdlExporter,或直接修改用于导出元数据的 WsdlExporter。
使用代理项来导出元数据
创建一个新 WsdlExporter 参数或使用
wsdlExporter
传递给 ExportContract 该方法的参数。使用该 TryGetValue 函数检查是否已定义一个 XsdDataContractExporter 。
如果TryGetValue返回
false
,请使用生成的 WsdlExporterXML 架构创建一个新XsdDataContractExporter架构,并将其添加到由 State 属性返回的WsdlExporter集合。 否则,请使用由out
方法的 TryGetValue 参数返回的导出程序。如果XsdDataContractExporter尚未定义ExportOptions,请将Options属性设置为ExportOptions类的新实例。
对于 DataContractSurrogate 的 ExportOptions,请将 XsdDataContractExporter 属性设置为该代理项的新实例。 导出元数据的后续步骤不需要进行任何更改。
WsdlExporter exporter = new WsdlExporter(); //or //public void ExportContract(WsdlExporter exporter, // WsdlContractConversionContext context) { ... } object dataContractExporter; XsdDataContractExporter xsdInventoryExporter; if (!exporter.State.TryGetValue(typeof(XsdDataContractExporter), out dataContractExporter)) { xsdInventoryExporter = new XsdDataContractExporter(exporter.GeneratedXmlSchemas); } else { xsdInventoryExporter = (XsdDataContractExporter)dataContractExporter; } exporter.State.Add(typeof(XsdDataContractExporter), xsdInventoryExporter); if (xsdInventoryExporter.Options == null) xsdInventoryExporter.Options = new ExportOptions(); xsdInventoryExporter.Options.DataContractSurrogate = new InventorySurrogated();