.NET XML 和 SOAP 序列化示例、技巧






4.83/5 (40投票s)
2005 年 7 月 22 日
10分钟阅读

272108

4781
提供使用 C# 进行 XML 和 SOAP 序列化的示例
引言
使用序列化,对象可以被持久化,这使得我们可以轻松地将它们存储在文件或数据库中。例如:存储应用程序的配置设置、存储应用程序的用户自定义设置、保存应用程序的状态。由于我们有了对象的持久化形式,我们可以将它们在应用程序之间传输。反序列化与序列化相反,它根据对象的持久化形式创建对象。
在序列化过程中,对象被序列化到流中。可以使用 FileStream
将对象持久化到文件中,也可以使用 MemoryStream
将对象的序列化形式存储在内存中。
.NET 支持 XML、SOAP 和二进制序列化。
XML 序列化
- 使用 XML 序列化时,只能序列化
public
属性和字段。如果要序列化private
成员,应使用其他序列化方法。 - 它需要一个无参构造函数。此构造函数在反序列化期间被调用。
- 对象的序列化形式不包含任何类型、类的程序集信息。只存储数据。
- 使用 XML 序列化时,并非所有类都可以被序列化。实现
IDictionary
的类不能被序列化。例如,Hashtable
不能被序列化。对象数组可以轻松地被序列化。集合可以被序列化,但要序列化集合,必须遵循某些规则。 - XML 序列化不会序列化
Null
值。要将null
成员包含在序列化形式中,应将IsNullable
属性设置为true
。例如:[ XmlElement( IsNullable = true ) ]
创建对象
让我们为序列化示例创建一个类。Exam
类包含一个头和一个问题集。
public class Exam{
public Exam(){
header=new Header();
}
private Header header;
private Question[] questions;
public Header Header{
get{return header;}
set{header=value;}
}
public Question[] Questions{
get{return questions ;}
set{questions=value;}
}
}
public class Header{
public Header(){}
private string title;
public string Title{
get{return title;}
set{title=value;}
}
}
public class Question{
public Question(){}
private int id;
private string[] items;
public int ID{
get{return id;}
set{id=value;}
}
public string[] Items{
get{return items;}
set{items=value;}
}
}
然后我们初始化对象。
Exam exam=new Exam();
exam.Header=new Header();
exam.Header.Title="Exam title";
exam.Questions=new Question[1];
exam.Questions[0]=new Question();
exam.Questions[0].ID=1;
exam.Questions[0].Title=
"What is your favourite serialization method?";
exam.Questions[0].Items=new string[2];
exam.Questions[0].Items[0]="Xml";
exam.Questions[0].Items[1]="Soap";
使用 XML 序列化将对象序列化到文件
public static void ToXml(Object objToXml,
string filePath,bool includeNameSpace) {
StreamWriter stWriter=null;
XmlSerializer xmlSerializer;
try {
xmlSerializer = new XmlSerializer(objToXml.GetType());
stWriter = new StreamWriter(filePath);
if (!includeNameSpace){
System.Xml.Serialization.XmlSerializerNamespaces xs=
new XmlSerializerNamespaces();
//To remove namespace and any
//other inline information tag
xs.Add("","");
xmlSerializer.Serialize(stWriter, objToXml,xs);
}
else{
xmlSerializer.Serialize(stWriter, objToXml);
}
}
catch(Exception exception){
throw exception;
}
finally {
if(stWriter!=null) stWriter.Close();
}
}
我们的方法接收要序列化对象的文件的路径。我们传递要序列化的对象,该方法会创建一个文件并将对象序列化。文件内容将是:
<?xml version="1.0" encoding="utf-8"?>
<Exam>
<Header>
<Title>Exam title</Title>
</Header>
<Questions>
<Question>
<ID>1</ID>
<Title>What is your favourite serialization method?</Title>
<Items>
<string>Xml</string>
<string>Soap</string>
</Items>
</Question>
</Questions>
</Exam>
从 XML 文件反序列化对象
以下方法可用于从文件中反序列化对象:
public static object XmlToFromFile(string filePath,
Type type) {
XmlSerializer xmlSerializer;
FileStream fileStream=null;
try {
xmlSerializer = new XmlSerializer(type);
fileStream = new FileStream(filePath,
FileMode.Open,FileAccess.Read);
object objectFromXml=
xmlSerializer.Deserialize(fileStream );
return objectFromXml;
}
catch(Exception Ex) {
throw Ex;
}
finally {
if(fileStream!=null) fileStream.Close();
}
}
使用 XML 序列化将对象序列化到内存中的字符串
有时,我们需要将对象的序列化形式存储在内存中。我们可以使用 MemoryStream
将对象的序列化形式存储在内存中。
public static string ToXml(Object objToXml,
bool includeNameSpace) {
StreamWriter stWriter=null;
XmlSerializer xmlSerializer;
string buffer;
try {
xmlSerializer =
new XmlSerializer(objToXml.GetType());
MemoryStream memStream = new MemoryStream();
stWriter = new StreamWriter(memStream);
if (!includeNameSpace){
System.Xml.Serialization.XmlSerializerNamespaces xs=
new XmlSerializerNamespaces();
//To remove namespace and any other inline
//information tag
xs.Add("","");
xmlSerializer.Serialize(stWriter, objToXml,xs);
}
else{
xmlSerializer.Serialize(stWriter, objToXml);
}
buffer = Encoding.ASCII.GetString(memStream.GetBuffer());
}
catch(Exception Ex){
throw Ex;
}
finally {
if(stWriter!=null) stWriter.Close();
}
return buffer;
}
从内存中的 XML 字符串反序列化对象
当然,您需要一个反序列化器来将内存中的 string
转换为对象!
public static object XmlTo(string xmlString,Type type) {
XmlSerializer xmlSerializer;
MemoryStream memStream=null;
try {
xmlSerializer = new XmlSerializer(type);
byte[] bytes=new byte[xmlString.Length];
Encoding.ASCII.GetBytes(xmlString,0,xmlString.Length,bytes,0);
memStream = new MemoryStream(bytes);
object objectFromXml= xmlSerializer.Deserialize(memStream);
return objectFromXml;
}
catch(Exception Ex) {
throw Ex;
}
finally {
if(memStream!=null) memStream.Close();
}
}
更改类
添加新属性
让我们向 header
类添加一个 description
属性。header
类如下所示:
public class Header{
public Header(){}
private string title;
private string description;
public string Title{
get{return title;}
set{title=value;}
}
public string Description{
get{return description;}
set{description=value;}
}
}
我们仍然可以反序列化对象而不会出现任何错误。序列化形式中不存在的值将设置为初始值。在这种情况下,description
属性被设置为 null
。
删除属性
现在,我们要从 Header
类中删除 description
属性。我们要反序列化一个文件中的对象,该文件在 header
部分具有 description
属性。我们会得到错误吗?我们可以成功反序列化而不会出现任何错误!删除或添加新属性不会导致 XML 序列化出现任何问题。
使用属性控制 XML 序列化
可以使用属性来控制序列化。可以标记一个属性以排除其序列化,或者可以将其序列化为 XML 属性或 XML 元素。SOAP/二进制序列化属性与 XML 序列化属性不同。示例中使用了一些属性来解释如何控制序列化。
序列化时更改属性的文本
假设您希望在序列化对象的文件中看到“QuestionTitle
”,而不是问题标签内的“Title
”标签。但您仍希望在代码中引用相同的属性。为此,应使用 XML 属性设置新值,如下所示:
[System.Xml.Serialization.XmlElementAttribute("QuestionTitle")]
public string Title{
get{return title;}
set{title=value;}
}
如果将此属性应用于数组属性,它将导致外部标签消失。
[System.Xml.Serialization.XmlElementAttribute("Item")]
public string[] Items{
get{return items;}
set{items=value;}
}
最终输出将是:
<?xml version="1.0" encoding="utf-8"?>
<Exam>
<Header>
<Title>Exam title</Title>
<Description>Exam description</Description>
</Header>
<Questions>
<Question>
<ID>1</ID>
<QuestionTitle>What is your favourite serialization method?
</QuestionTitle>
<Item>Xml</Item>
<Item>Soap</Item>
</Question>
</Questions>
</Exam>
向类添加 ArrayList
让我们修改 Exam
类,以便可以使用 ArrayList
来存储问题,而不是使用数组。
public class Exam{
public Exam(){
header=new Header();
questions=new ArrayList();
}
private Header header;
private ArrayList questions;
public Header Header{
get{return header;}
set{header=value;}
}
public ArrayList Questions{
get{return questions ;}
set{questions=value;}
}
}
当我们运行示例时,我们会收到 InvalidOperationException
错误,其中包含“未预期的类型 SerializationSamples.Question
。请使用 XmlInclude
或 SoapInclude
属性指定静态未知类型。”的内部异常。
我们可以使用 XmlArrayItem
属性指定 ArrayList
中的类型。它允许我们在序列化类时使用 ArrayList
。
[XmlArrayItem(typeof(Question))]
public ArrayList Questions{
get{return questions ;}
set{questions=value;}
}
现在,序列化工作照常进行,生成相同的输出到文件。
自定义序列化
有时,我们需要控制 XML 序列化。在这种情况下,我们可以重写序列化以更改其默认行为。我们创建自己的 StreamWriter
并在序列化时使用此类。例如,让我们从 XML 输出中删除 XML 标签。这样我们就不会在输出中看到“<?xml version="1.0" encoding="utf-8"?>
”行了。
示例:删除 XML 版本行
要控制序列化过程中的所有内容,必须实现 ISerializable
接口,而不是使用 .NET 序列化。
让我们编写该类。它称为 SpecialXmlWriter
,并继承自 XmlTextWriter
。其构造函数接受一个参数来包含 XML 版本行,并重写了 WriteStartDocument
。
public class SpecialXmlWriter:XmlTextWriter
{
bool m_includeStartDocument=true;
public SpecialXmlWriter(TextWriter tw,
bool includeStartDocument):base(tw) {
m_includeStartDocument=includeStartDocument;
}
public SpecialXmlWriter(Stream sw,Encoding encoding,
bool includeStartDocument):base(sw,null) {
m_includeStartDocument=includeStartDocument;
}
public SpecialXmlWriter(string filePath,Encoding encoding,
bool includeStartDocument):base(filePath,null) {
m_includeStartDocument=includeStartDocument;
}
public override void WriteStartDocument() {
if (m_includeStartDocument) {
base.WriteStartDocument();
}
}
现在让我们编写另一个重载的序列化方法。在此方法中,我们使用了 SpecialXmlWriter
类而不是 StreamWriter
,并将 includeStartDocument
参数传递给了 SpecialXmlWriter
的构造函数。
public static void ToXml(Object objToXml,
string filePath,
bool includeNameSpace,
bool includeStartDocument) {
SpecialXmlWriter stWriter=null;
XmlSerializer xmlSerializer;
try {
xmlSerializer =
new XmlSerializer(objToXml.GetType());
stWriter = new SpecialXmlWriter(filePath,null,
includeStartDocument);
System.Xml.Serialization.XmlSerializerNamespaces xs=
new XmlSerializerNamespaces();
//To remove namespace and any other
//inline information tag
xs.Add("","");
xmlSerializer.Serialize(stWriter, objToXml,xs);
}
catch(Exception Ex) {
throw Ex;
}
finally {
if(stWriter!=null) stWriter.Close();
}
}
SOAP 序列化
- 在 SOAP 和二进制序列化期间,也会序列化
private
字段、类型信息(包括程序集、命名空间、公钥标记信息)。 - 反序列化会得到与序列化之前相同的对象。
- 可以使用 [
NonSerialized
] 属性来控制序列化,以排除某个字段。 - [
Serializable
] 属性用于标记一个类为可序列化。使用 XML 序列化时,不必添加它。当一个类被标记为具有此属性时,运行时会完成所有工作。但是,它必须为每个要序列化的类添加。类不会从其基类继承此属性。要序列化一个类,该类的所有成员都必须是可序列化的,否则它将无法被序列化。 - 要使用 SOAP 和二进制序列化,类不需要具有无参构造函数。由于性能问题,在反序列化期间不会调用类的构造函数。
- 使用 XML 序列化无法序列化的
IDictionary
对象可以使用 SOAP/二进制序列化来序列化。- 二进制序列化
- 紧凑的序列化
- 性能更好
- 创建字节流
- SOAP 序列化
- 创建 SOAP 消息
- 如果序列化和反序列化平台不是 .NET,则使用此选项。
- 如果消息通过防火墙发送,则使用此选项。
- 易于调试
- 二进制序列化
SOAP 和二进制格式化器都继承自 IFormatter
接口。本文中的 SOAP 示例可以通过仅更改格式化器的创建来轻松更改为二进制序列化示例,如下所示:
Formatter=new BinaryFormatter();
使用 SOAP 序列化将对象序列化到文件
public static string ToSoap(Object objToSoap,
string filePath) {
IFormatter formatter;
FileStream fileStream=null;
string strObject="";
try{
fileStream = new FileStream(filePath,
FileMode.Create,FileAccess.Write);
formatter = new SoapFormatter();
formatter.Serialize(fileStream, objToSoap);
}
catch(Exception exception){
throw exception;
}
finally{
if (fileStream!=null) fileStream.Close();
}
return strObject;
}
我们的方法接收文件路径和要序列化的对象,然后创建具有如下所示内容的文件。
<SOAP-ENV:Envelope
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<a1:Exam id="ref-1"
xmlns:a1="http://schemas.microsoft.com/clr/nsassem/
SerializationSamples.SOAP/SerializationSamples%2C%20Version%3D1.0.0.0
%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<header href="#ref-3"/>
<questions href="#ref-4"/>
</a1:Exam>
<a1:Header id="ref-3"
xmlns:a1="http://schemas.microsoft.com/clr/nsassem/
SerializationSamples.SOAP/SerializationSamples%2C%20Version
%3D1.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<title id="ref-5">Exam title</title>
<description id="ref-6">Exam description</description>
</a1:Header>
<a2:ArrayList id="ref-4"
xmlns:a2="http://schemas.microsoft.com/clr/ns/System.Collections">
<_items href="#ref-7"/>
<_size>1</_size>
<_version>1</_version>
</a2:ArrayList>
<SOAP-ENC:Array id="ref-7" SOAP-ENC:arrayType="xsd:anyType[16]">
<item href="#ref-8"/>
</SOAP-ENC:Array>
<a4:Question id="ref-8"
xmlns:a4="http://schemas.microsoft.com/clr/nsassem/
SerializationSamples/SerializationSamples%2C%20Version
%3D1.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<id>1</id>
<title id="ref-9">What is your favourite serialization method?</title>
<items href="#ref-10"/>
</a4:Question>
<SOAP-ENC:Array id="ref-10" SOAP-ENC:arrayType="xsd:string[2]">
<item id="ref-11">Xml</item>
<item id="ref-12">Soap</item>
</SOAP-ENC:Array>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
从 SOAP 文件反序列化对象
以下方法可用于从 SOAP 文件反序列化对象:
public static object SoapToFromFile(string filePath) {
IFormatter formatter;
FileStream fileStream=null;
Object objectFromSoap=null;
try{
fileStream = new FileStream(filePath,
FileMode.Open,FileAccess.Read);
formatter = new SoapFormatter();
objectFromSoap=formatter.Deserialize(fileStream);
}
catch(Exception exception){
throw exception;
}
finally{
if (fileStream!=null) fileStream.Close();
}
return objectFromSoap;
}
使用 SOAP 序列化将对象序列化到内存中的字符串
public static string ToSoap(Object objToSoap) {
IFormatter formatter;
MemoryStream memStream=null;
string strObject="";
try{
memStream = new MemoryStream();
formatter = new SoapFormatter();
formatter.Serialize(memStream, objToSoap);
strObject =
Encoding.ASCII.GetString(memStream.GetBuffer());
//Check for the null terminator character
int index=strObject.IndexOf("\0");
if (index>0){
strObject=strObject.Substring(0,index);
}
}
catch(Exception exception){
throw exception;
}
finally{
if (memStream!=null) memStream.Close();
}
return strObject;
}
从 SOAP 字符串在内存中反序列化对象
public static object SoapTo(string soapString) {
IFormatter formatter;
MemoryStream memStream=null;
Object objectFromSoap=null;
try{
byte[] bytes=new byte[soapString.Length];
Encoding.ASCII.GetBytes( soapString, 0,
soapString.Length, bytes, 0);
memStream = new MemoryStream(bytes);
formatter = new SoapFormatter();
objectFromSoap=
formatter.Deserialize(memStream);
}
catch(Exception exception){
throw exception;
}
finally{
if (memStream!=null) memStream.Close();
}
return objectFromSoap;
}
在反序列化之前更改类
添加新属性或更改命名空间不会影响 XML 序列化。但如果使用 SOAP/二进制序列化,您需要非常小心。SOAP/二进制序列化直接处理成员,并且还序列化类型信息。在 .NET 中,类型信息还包括程序集名称和命名空间。更改其中任何一个都可能影响应用程序。添加新成员或更新现有成员可能会导致问题。可能无法将对象反序列化回来。
添加属性
让我们向 Exam
类添加一个类型为 string
的新属性,并尝试从使用前一个类版本序列化的文件反序列化对象。我们添加了 Author
属性。我们的 Exam
类变为如下:
[Serializable]
public class Exam{
public Exam(){
header=new Header();
questions=new ArrayList();
}
private Header header;
private ArrayList questions;
private string author;
public Header Header{
get{return header;}
set{header=value;}
}
public ArrayList Questions{
get{return questions ;}
set{questions=value;}
}
public string Author{
get{return author ;}
set{author=value;}
}
}
当我们运行应用程序时,我们会收到以下异常:“在 SerializationSamples.exe 中发生了类型为 System.Runtime.Serialization.SerializationException 的未处理异常
”。
附加信息:“成员数量错误。对象 SerializationSamples.SOAP.Exam 包含 3 个成员,反序列化的成员数量为 2
”。
SOAP 序列化会序列化类的所有成员,并在序列化时查找所有这些成员。
为了克服这个问题,让我们从头开始。我们删除 Author
属性以重新开始。我们添加一个名为“properties
”的 Hashtable
,以备将来增强我们的类,无论我们是否使用它。目前我们不使用此 Hashtable
,但它会被序列化为一个空的 Hashtable
。将来,如果我们想添加一个新属性,我们将从 hashtable
中存储和获取它们。由于 Hashtable
已经被序列化,新添加的属性可以成功地被反序列化和再次序列化。如果属性未存储在 Hashtable
中,则可以将其设置为默认值。
我们的类变为如下:
[Serializable]
public class Exam{
public Exam(){
header=new Header();
questions=new ArrayList();
}
private Header header;
private ArrayList questions;
private Hashtable properties;
public Header Header{
get{return header;}
set{header=value;}
}
public ArrayList Questions{
get{return questions ;}
set{questions=value;}
}
}
现在,向 Exam
类添加 Hashtable
,初始化它并序列化该类,以便我们可以测试下一步,了解是否仍然可以添加新属性。
public Exam(){
header=new Header();
questions=new ArrayList();
properties=new Hashtable();
}
private Hashtable properties;
让我们添加 Author
属性并尝试从现有文件反序列化对象。Author
属性使用 Hashtable
来存储和获取其值。它不使用类成员,否则反序列化会失败。
public string Author{
get{
if (properties[PROPERTYNAME.AUTHOR]==null){
return "";
}
else{
return (string)properties[PROPERTYNAME.AUTHOR];
}
}
set{
properties[PROPERTYNAME.AUTHOR]=value;
}
}
以及 PROPERTYNAME
枚举:
public enum PROPERTYNAME{
AUTHOR=0
}
在向类添加新属性时,我们会向 PROPERTYNAME
枚举添加一个新的 enum
,并使用此值作为键来在 Hashtable
中获取和存储值。
更改类型信息
如果我们想更改类的类型信息会怎样?在类被序列化时,类型信息也会被序列化,从而使类能够使用类型信息进行反序列化。类型信息包括命名空间、类名、程序集名称、区域性信息、程序集版本和公钥标记。只要您的反序列化类和被序列化的类位于同一个程序集中,就不会出现任何问题。但如果序列化器位于单独的程序集中,.NET 将找不到您的类的类型,因此无法反序列化它。
如果更改类的程序集名称,您将收到以下错误,指示 .NET 找不到关联的程序集:“在 serializationsamplesa.dll 中发生了类型为 System.Runtime.Serialization.SerializationException 的未处理异常
”。
附加信息:“解析错误,XML 键 a1:http://schemas.microsoft.com/clr/nsassem/SerializationSamples.SOAP/ SerializationSamples%2C%20Version%3D1.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull Exam 找不到关联的程序集
”。
如果更改类的命名空间,您将收到以下错误,指示 .NET 找不到关联的类型:“在 serializationsamples.dll 中发生了类型为 System.Runtime.Serialization.SerializationException 的未处理异常
”。
附加信息:“解析错误,XML 键 a1 SerializationSamples.SOAP.Exam SerializationSamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null 找不到关联的类型
”。
如果很可能进行更改,那么最好实现 ISerializable
接口而不是使用 Serializable
属性。如果类实现了 ISerializable
接口,则所有序列化都必须手动编码。
在这种情况下,我们可以编写自己的类的绑定器(binder),并在查找类的类型时告诉反序列化器使用我们的绑定器。对于每个被序列化或类型信息记录在 SOAP 消息中的类,我们需要返回其类型信息,以便可以加载正确的类型。在下面的代码的最后一个 else
语句中,我们返回默认的传入类型信息。在我们的例子中,这些是 Object
、ArrayList
和 Hashtable
类型,因为它们是我们类使用的系统类。
public class ExamBinder:
System.Runtime.Serialization.SerializationBinder {
public override Type BindToType(string assemblyName,
string typeName) {
string[] typeInfo=typeName.Split('.');
//The latest item is the class name
string className=typeInfo[typeInfo.Length -1];
if (className.Equals("Exam")){
return typeof (Exam);
}
else if (className.Equals("Header")){
return typeof (Header);
}
if (className.Equals("Question")){
return typeof (Question);
}
else{
return Type.GetType(string.Format( "{0}, {1}",
typeName, assemblyName));
}
}
}
现在让我们创建另一个重载的反序列化方法。它接收一个绑定器参数,并将此参数传递给格式化器的 binder
属性。
public static object SoapToFromFile(string filePath,
SerializationBinder binder) {
IFormatter formatter;
FileStream fileStream=null;
Object objectFromSoap=null;
try{
fileStream = new FileStream(filePath,
FileMode.Open, FileAccess.Read);
formatter = new SoapFormatter();
formatter.Binder=binder;
objectFromSoap=formatter.Deserialize(fileStream);
}
catch(Exception exception){
throw exception;
}
finally{
if (fileStream!=null) fileStream.Close();
}
return objectFromSoap;
}
可以如下所示调用它:
System.Runtime.Serialization.SerializationBinder binder=
new ExamBinder();
Exam soapExamFromBinder =
XmlUtil.SoapToFromFile(txtFilePath.Text,binder) as Exam;