Windows Communication Foundation 中的序列化






4.24/5 (11投票s)
本文档描述了 Windows Communication Foundation 中的序列化和消息编码。
引言
在大多数情况下,当我们设计一个服务时,我们会为服务中使用的类型创建服务协定和数据协定。我们让服务描述由服务协定生成,并且永远不需要查看底层 XML,但有时,情况并不总是那么顺利,例如预先存在的类型实现了 SerializableAttribute
和 ISerializable
,将传入消息映射到不可序列化的类型或不符合预定义架构的类型。
在内部,WCF 使用 System.ServiceModel.Channels
命名空间中定义的 Message
类来表示所有消息。Message
类生成一个 SOAP 信封,该信封由消息头和消息体组成。消息体包含有效载荷。Message
类提供了一个接口来与消息体或消息头进行交互。
在 WCF 服务中最常见的序列化方式是使用 DataContract
属性标记类型,并使用 DataMember
属性标记要序列化的每个成员。
背景
读者应熟悉 WCF,以及如何创建 WCF 服务和数据协定。读者还应该了解 Web 服务。
支持序列化的类型
- 标记有
DataContractAttribute
或CollectionDataContractAttribute
的类型。 - 使用
SerializableAttribute
装饰的类型,这些类型可以选择实现ISerializable
或IXmlSerializable
接口。 - 使用
MessageContractAttribute
装饰的MessageContract
,它包含数据协定或可序列化的类型作为头或体。 - 我们可以包含这些类型中的任何一种,但最终的消息格式将根据序列化器的选择而改变。
有两种序列化器:DataContractSerializer
和 XmlSerializer
。默认情况下,WCF 使用 DataContractSerializer
进行消息序列化。以下是这两种序列化器的区别。
- 对于
DataContract
,DataContractSerializer
序列化标记有DataMember
属性的字段(无论公共还是私有),而XmlSerializer
只序列化公共属性和字段。 - 对于
SerializableAttribute
,DataContractSerializer
序列化所有字段,而XmlSerializer
只序列化公共字段。 - 对于
IXmlserializable
接口,我们提供架构并控制序列化。 - 对于
MessageContract
,DataContractSerializer
序列化标记有 header 或 body 属性的成员,而XmlSerializer
只序列化公共属性或字段。 - 对于
DataContractSerializer
,在反序列化过程中不会调用构造函数。但是,如果您需要执行初始化,DataContractSerializer
支持[OnDeserializing]
、[OnDeserialized]
、[OnSerializing]
和[OnSerialized]
回调,这些回调也受Binary/SoapFormatter
类支持。
消息编码
存在两种编码方式:Document/literal 和 RPC。实际上,它们是两种编程模型,一种是 RPC 编程模型,另一种是消息传递编程模型。一种编程模型认为,如果您正在与服务通信,您应该始终显式地形成一个消息,并使用显式的消息传递基础结构将消息发送到服务,并可选地接收响应。以下是这种机制潜在实现的一个示例(从调用者的角度来看)
Channel chan = new ChannelFactory.CreateChannel(endpointAddress);
ActionInvocationMessage msg = new ActionInvocationMessage();
msg.Action = "SayHello";
msg.Parameters.Add(new StringParameter("Forename", "Rich"));
msg.Parameters.Add(new StringParameter("Surname", "Turner"));
Console.WriteLine(chan.SendMessageSync(msg, new SayHelloCallback(HandleResponse)));
另一个阵营旨在通过重用当今广泛使用的开发构造来简化开发人员的体验,这会导致生成如下代码
HelloWorldProxy proxy = new HelloWorldProxy(endpointAddress);
Console.WriteLine(proxy.SayHello("Rich", "Turner"));
在 DataContractSerializer
和 XmlSerializer
中,style
的默认值是 document/literal 格式,但如果需要,我们可以使用 OperationFormatStyle
枚举来使用 RPC 编码。每种序列化器都有一个基于该枚举的 style
属性,可以设置为其两个元素之一:Document
和 RPC
。
DataContractFormatAttribute
我们可以这样使用 RPC 风格
[ServiceContract(Name="ServiceContract",
Namespace="http://www.ServiceContract.com/Samples/RPCExample")]
[DataContractFormat(Style=OperationFormatStyle.Rpc)]
public interface IRPCStyleTest
XmlSerializerFormatAttribute
在 XmlSerializer
的情况下,我们可以这样支持 RPC 风格
[ServiceContract(Name="ServiceContract",
Namespace="http://www.ServiceContract.com/Samples/RPCExample")]
[XmlSerializerFormat(Style=OperationFormatStyle.Rpc,
Use=OperationFormatUse.Encoded)]
public interface IRPCStyleTest
序列化/编码
实际上,在讨论 .NET 类型与序列化相关时,存在两个协定。第一个是 .NET 协定,它具有属性、方法和构造函数;另一个是具有可序列化属性并将在外部公开的协定。每种序列化器都带有一些算法,这些算法定义了映射细节,并包括允许我们自定义映射的属性。这种映射与序列化器在运行时可以访问的元数据一起存储,并决定序列化哪个属性。
存在一个内置工具(svc.exe),用于在这些协定表示之间移动。在处理 XmlSerializer
时,我们可以使用 xsd.exe。Svc.exe 可以从 XML Schema 定义生成具有所有属性的 .NET 数据类型。如果我们向 svc.exe 传递一个 .NET 程序集,它将自动生成所有支持序列化的类型。因此,我们可以用 XML Schema 或 .NET 代码编写数据协定。
Windows Communication Foundation 允许我们指定要使用的编码。序列化定义了 .NET 对象如何映射到 XML,而编码定义了 XML 如何写入字节流。WCF 支持三种编码类型:文本、二进制和消息传输优化机制 (MTOM)。我们还可以添加自定义编码。
如果我们使用这三种编码表示相同的逻辑数据,那么流将完全不同,但它们都代表相同的逻辑数据。编码不被视为服务协定的一部分,而是配置项。我们可以动态更改它,并且可以通过终结点绑定来控制它。不同类型的编码用于不同类型的场景。如果关心互操作性,则使用文本类型编码;如果关心性能,则可以使用二进制编码。
当 WCF 在运行时创建消息时,它会根据服务协定选择序列化器,并根据绑定配置选择编码。让我们看看这是如何实现的。我们可以使用以下类型来理解序列化器
[DataContract]
public class Name
{
[DataMember]
public string firstName;
public string middlename;
[DataMember]
public string lastName;
}
DataContractSerializer
我们可以使用以下方法从 DataContractSerializer
生成序列化流
public static void SerializeClass()
{
Name name = new Name { firstName = "John",
middlename = "Fuma", lastName = "last" };
using (Message message = Message.CreateMessage(MessageVersion.Soap11WSAddressing10,
"http://www.ServiceContract.com/Samples/RPCExample/Name", name,
new DataContractSerializer(typeof(Name))))
{
using (FileStream fs = new FileStream("binary.bin", FileMode.Create))
{
using (XmlDictionaryWriter writer =
XmlDictionaryWriter.CreateBinaryWriter(fs))
{
message.WriteMessage(writer);
}
}
}
}
CreateMessage
方法的最后一个参数指定了序列化器。序列化器控制消息体如何作为 XML 具有 Name
对象。写入器决定使用哪种编码将消息写入字节流。XmlDictionaryWriter
和 XmlDictionaryReader
可用于以任何编码(文本、二进制、MTOM)读取和写入类型。
这是 Visual Studio 中生成的文件的视图
可以使用以下方法从bin文件再次生成 Name
类型。我们可以使用 XmlDictionaryReader
和相应的解码器进行逆向工程,从而获取原始消息。
public static void ReadSerializeClass()
{
using (FileStream fs = File.OpenRead("binary.bin"))
{
using (XmlDictionaryReader reader =
XmlDictionaryReader.CreateBinaryReader(fs, XmlDictionaryReaderQuotas.Max))
{
using (Message message = Message.CreateMessage(
reader, 1024, MessageVersion.Soap11WSAddressing10))
{
// deserialize Person object from XML
Name name = message.GetBody(
new DataContractSerializer(typeof(Name)));
}
}
}
}
如果我们不想直接读取 XML 信息集,我们可以调用 GetReaderAtBodyContents
而不是 GetBody
,它返回 XmlReader
并读取特定元素。
XmlSerializer
XmlSerializer
是一项相当古老的技术,用于 Web 服务。Windows Communication Foundation 支持它以实现向后兼容。我将不会详细讨论它,因为已经有很多关于它的文章了。下面是序列化相同 Name
对象的示例代码。我们可以看到它还包括了中间名,因为中间名是公共属性。
序列化方法
public static void WriteNameXmlSerializer()
{
Name name = new Name { firstName = "John",
middlename = "Fuma", lastName = "last" };
using (FileStream fs = new FileStream("Name.xml", FileMode.Create))
using (XmlDictionaryWriter writer =
XmlDictionaryWriter.CreateTextWriter(fs))
{
XmlSerializer serializer = new XmlSerializer(typeof(Name));
serializer.Serialize(writer, name);
}
}
序列化流
<Name xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<firstName>John</firstName>
<middlename>Fuma</middlename>
<lastName>last</lastName>
</Name>
反序列化方法
public static void ReadNameXmlSerializer()
{
using (FileStream fs = new FileStream("Name.xml", FileMode.Open))
using (XmlDictionaryReader reader =
XmlDictionaryReader.CreateTextReader(fs, XmlDictionaryReaderQuotas.Max))
{
XmlSerializer serializer = new XmlSerializer(typeof(Name));
Name person = (Name)serializer.Deserialize(reader);
}
}
NetDataContractSerializer
DataContractSerializer
有两种模式:SharedContract
和 SharedType
。这两种模式反映了您是想在网络上传输共享的架构协定还是 .NET 类型信息。
通常,共享架构协定是首选和推荐的方法,因为它降低了网络通信的耦合度,但 WCF 仍然希望支持 .NET Remoting 风格的应用程序,在这些应用程序中,在网络上传输类型很重要。WCF 将这两种模式分成了独立的类,现在 WCF 有 DataContractSerializer
(SharedContract
) 和 NetDataContractSerializer
(SharedType
)。由于 WCF 希望推广 DataContractSerializer
,所以它是默认的序列化器。
NetDataContractSerializer
支持相同的映射算法和相同的自定义属性,但在 NetDataContractSerializer
的情况下,不需要像 DataContractSerializer
那样提前准备类型信息。我们可以从以下代码中看到这一点
// no type specified
NetDataContractSerializer serializer = new NetDataContractSerializer();
serializer.WriteObject(writer, p);
我们可以使用以下方法序列化 Name
类
public static void SerializeClass()
{
Name name = new Name { firstName = "John", middlename = "Fuma", lastName = "last" };
NetDataContractSerializer netData = new NetDataContractSerializer();
using (FileStream fs = new FileStream("NetData.xml", FileMode.Create))
{
netData.Serialize(fs, name);
}
}
这是序列化流。它包含类型信息,因此在进行反序列化时无需指定类型。
<Name z:Id="1" z:Type="HelloIndigo.Name" z:Assembly="HelloIndigo,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
xmlns="http://schemas.datacontract.org/2004/07/HelloIndigo"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
<firstName z:Id="2">John</firstName>
<lastName z:Id="3">last</lastName>
</Name>
以下是在进行序列化时需要注意的一些事项
- 序列化使用继承的类型时,层次结构中的所有类型都应通过
DataContract
或Serializable
属性进行序列化。 - WCF 还提供了一个
KnowType
属性,可用于替换序列化器在运行时识别的类型。这允许我们为某个基类型指定Serializable
属性,并发送某个派生类型。 - 如果我们发现某个属性不可序列化,可以使用
IDataContractSurrogate
。我们可以实现此接口,序列化器在进行序列化时会调用它。
结论
Windows Communication Foundation 会处理所有事情,但了解每种序列化器的工作方式以及哪种最适合特定场景非常重要。应尽可能在 WCF 中使用 DataContractSerializer
。它提供了互操作性。当您需要在网络上传输类型保真度时,应使用 NetDataContractSerializer
。当您需要与 ASMX 页面向后兼容,或者您正在使用不遵循 DataContract
规则的现有类型时,应使用 XmlSerializer
。