第10章 LINQ to XML
10.1 架构概述——DOM 和 LINQ to XML 的 DOM
XML 文档可以用一棵对象树完整的表示,这称为“文档对象模型(document object model)”
LINQ to XML 由两部分组成:
- XML DOM,简称为 X-DOM
- 大约 10 个查询运算符
LINQ 也可以用于查询 W3C 标准的旧 DOM,不过 X-DOM 对 LINQ 查询更为友好:
- X-DOM 部分方法可以返回
IEnumerable
序列; - X-DOM 的构造器支持通过 LINQ 构建对象树。
Tips
W3C 标准的 DOM 对应 C# 中的
XmlDocument
;X-DOM 对应 C# 中的XDocument
等一系列类型。更多内容见12 System.Xml 的使用
10.2 X-DOM 概览
XObject
是所有类型的基类,XElement
和 XDocument
是所有容器类型的基类
以如下代码为例,它对应的 X-DOM 如图:
string xml = @"
<customer id='123' status='archived'><firstname>Joe</firstname><lastname>Bloggs<!--nice name--></lastname>
</customer>
";XElement customer = XElement.Parse(xml);
XObject
XObject
为抽象类,是所有 XML 内容(XML Content)的 基 类,它内含一个指向 父 元素( Parent element)的属性、一个指向 XDocument
的(可选)属性。
XNode
XNode
为抽象类,是大多数 XML 内容的基类(不含 XAttribute
)。XNode
指向 父 元素(Element),不会指向 子 节点(Node)。
XContainer
我们在 XNode 提到,XNode
不会指向 子 节点。指向 子 节点的工作由它的派生类 XContainer
完成。
XContainer
为抽象类,用于处理子项。它也是 XElement
和 XDocument
的基类。
XElement
XElement
引入了诸如 Name
、Value
等成员,用于管理特性。多数 XElement
仅包含一个 XText
节点,Value
用于快捷地 get、set 其内容。
XDocument
XDocument
封装了根节点的 XElement
,添加了 XDeclaration
、一系列处理指令及其他根元素+功能。
与 W3C DOM 不同,XDocument
是可选的,因此我们可以高效移动任意子节点至其他 X-DOM 中。
10.2.1 加载和解析
XElement
和 XDocument
提供了静态 Load
和 Parse
方法,用于从现有源建立 X-DOM 树。支持的源有:
-
Load
方法:通过文件建立 X-DOM:URI、Stream、
TextReader
、XmlReader
XDocument fromWeb = XDocument.Load ("http://albahari.com/sample.xml"); XElement fromFile = XElement.Load (@"e:\media\somefile.xml"); XElement config = XElement.Parse (@" <configuration><client enabled='true'><timeout>30</timeout></client> </configuration>");
-
Parse
方法:通过字符串建立 X-DOM
Tips
XNode
也提供了一个静态方法 ReadFrom
,从 XmlReader
中实例化+填充任意类型的节点(node)。与Load
不同,它每次仅读取一个完整节点,因此我们可以用它进行手动读取。
10.2.2 保存和序列化
任何 node 实例都可以通过其 ToString
方法输出 XML 格式的字符串,通过 WriteTo
方法将数据写入 XmlWriter
中。
Tips
通过
ToString
获得的字符串包含缩进、换行等格式化内容,可以通过传入SaveOptions.DisableFormatting
参数关闭该特性。注意:若原始 XML 内容包含格式化内容,即使传入
SaveOptions.DisableFormatting
参数,仍会保持原缩进样式。
XElement
和 XDocument
提供了 Save
方法将 X-DOM 保存至 URI、Stream、TextWriter
、XmlWriter
中。该方法会自动添加 XML 声明(见10.7.2 XML 声明(declaration))。
10.3 实例化 X-DOM
10.3.0 构造器 + Add
方法
任意 XContainer
的子类都可以使用构造器 + Add
方法创建 X-DOM 树,方法如下:
XElement lastName = new XElement ("lastname", "Bloggs");
lastName.Add (new XComment ("nice name"));XElement customer = new XElement ("customer");
customer.Add (new XAttribute ("id", 123));
customer.Add (new XElement ("firstname", "Joe"));
customer.Add (lastName);customer.Dump();
<customer id="123"><firstname /><lastname>Bloggs<!--nice name--></lastname>
</customer>
其中 Name
参数必选, Value
参数可选(可以在创建完成后再设置 Value
值)。Value
对应 XText
节点,它会被隐式创建。
10.3.1 函数式构建(Functional Construction)
X-DOM 支持“函数式”构建(源自函数式编程 functional programming),用法如下:
new XElement ("customer", new XAttribute ("id", 123),new XElement ("firstname", "joe"),new XElement ("lastname", "bloggs",new XComment ("nice name"))
)
Eureka
XElement
利用了params
关键字实现该效果:public XElement(XName name, params object?[] content)
优点有2:
-
和 XML 自身结构相似;
-
它可以使用 LINQ 的
select
语句。以如下代码为例,其中
Customers
为 EF Core 实例。new XElement ("customers",from c in Customers.AsEnumerable()select new XElement ("customer",new XAttribute ("id", c.ID),new XElement ("name", c.Name,new XComment ("nice name"))) )
10.3.2 指定内容(Specifying Content)
实际上,10.3.1 函数式构建(Functional Construction)利用了 C# 中的可选参数数组,XElement
的构造器和 XContainer
的 Add
方法定义如下:
public XElement(XName name, params object?[] content)
public void Add (params object[] content)
XContainer
将可选参数数组的所有对象都转为了 Node 或 Attribute ,其处理逻辑如下:
- 忽略 null 对象;
-
XNode
、XStreamingElement
对象,添加至 Node 集合中; -
XAttribute
对象,添加至 Attribute 集合中; -
string
对象,包装成 XText
节点,添加至 Node 集合中; -
IEnumerable
对象,遍历所有内容,按照 1~4 步处理; - 其他:将对象转化为
string
,按照步骤 4 处理。
Tips
object
包含ToString
方法,所有object
都可以转化为XText
节点,因此不存在无效对象。此外,
XContainer
在调用ToString
前会检查对象是否是如下类型,是则调用XmlCovert
,保证序列化不受CultureInfo
影响,符合 XML 格式规则:
float
、double
、decimal
、bool
、DateTime
、DateTimeOffset
、TimeSpan
10.3.3 自动深度克隆(Automatic Deep Cloning)
如XObject中提到,所有元素都包含 Parent Element 指针。当实例已有 Parent,将其赋值给其他 XContainer
时,将自动进行深克隆:
var address =new XElement ("address",new XElement ("street", "Lawley St"), new XElement ("town", "North Beach"));var customer1 = new XElement ("customer1", address);
var customer2 = new XElement ("customer2", address);customer1.Element ("address").Element ("street").Value = "Another St";
customer2.Element ("address").Element ("street").Value.Dump(); // 输出 Lawley St
Extra
因 X-DOM 深拷贝的特性,它的实例化没有任何副作用,这也是“函数式编程”的特点。
10.4 导航和查询(Navigating and Querying)
XNode
和 XContainer
定义了方法和属性用于游历 X-DOM 树。与常规 DOM 不同,这些函数返回单个值或 IEnumerable<T>
对象,而非 IList<T>
,因此需要通过 LINQ 进行查询。
Warn
在 X-DOM 中,Element 和 Attribute 的 name 是大小写敏感的,与 XML 一致。
10.4.1 导航至子节点
C 12 in a Nutshell The Definitive Reference.pdf - p547 - C 12 in a Nutshell The Definitive Reference-P547-20240426173112-6bd6490
Tips
带 * 的方法可以在序列(sequences)上使用(由 LINQ 支持)。
Info
本节用到的 XML 内容均为:
<bench><toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox><toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox><!--Be careful with the nailgun--> </bench>
10.4.1.1 FirstNode()
、LastNode()
和 Nodes()
这三个方法(属性)用于操作 直接 子节点,Nodes()
返回 所有直接子节点序列(sequences )。以如下代码为例:
var bench =new XElement ("bench",new XElement ("toolbox",new XElement ("handtool", "Hammer"),new XElement ("handtool", "Rasp")),new XElement ("toolbox",new XElement ("handtool", "Saw"), new XElement ("powertool", "Nailgun")),new XComment ("Be careful with the nailgun"));bench.FirstNode.ToString(SaveOptions.DisableFormatting).Dump ("FirstNode");
bench.LastNode.ToString(SaveOptions.DisableFormatting).Dump ("LastNode");foreach (XNode node in bench.Nodes())Console.WriteLine (node.ToString (SaveOptions.DisableFormatting) + ".");
FirstNode:
<toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>LastNode:
<!--Be careful with the nailgun-->Nodes():
<toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>.
<toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox>.
<!--Be careful with the nailgun-->.
Tips
FirstNode
和LastNode
的返回值类型为 XNode
,Nodes
的返回值类型为 IEnumerable<XNode>
。
10.4.1.2 检索 elements
Elements()
方法返回 XElement
类型的单层子节点:
foreach (XNode node in bench.Elements("handtool"))Console.WriteLine(node.ToString(SaveOptions.DisableFormatting) + ".");
<toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>.
<toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox>.
Elements()
可以返回指定名称的元素:
int toolboxCount = bench.Elements ("toolbox").Count();
Summary
从上面两个例子可以看出,
Nodes()
与Elements()
的区别:
-
Nodes()
不 支持寻找指定元素;-
Elements()
只列出 XElement
成员。
Elements()
与 LINQ
<bench><toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox><toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox><!--Be careful with the nailgun--> </bench>
如下代码查询含有 Nailgun
的 toolBox
:
var toolboxWithNailgun =from toolbox in bench.Elements()where toolbox.Elements().Any (tool => tool.Value == "Nailgun")select toolbox.Value;
如下代码查询所有 handtool
:
var handTools =from toolbox in bench.Elements()from tool in toolbox.Elements()where tool.Name == "handtool"select tool.Value;
如下代码返回指定名称的元素:
var count = bench.Elements().Where(e => e.Name == "toolbox").Count();
等价于:
int toolboxCount = bench.Elements ("toolbox").Count();
Elements()
与 IEnumerable<T> where T : XContainer
XContainer.Elements()
方法的 LINQ 查询与 XContainer.Nodes()
方法的 LINQ 查询等价,之前的示例还可以写为:
from toolbox in bench.Nodes().ofType<XElement>()
where ...
但是 XContainer
有额外的扩展方法,XElement
作为它的子类同样可以用它处理元素序列。使用方式形下:
var handTools2 =from tool in bench.Elements ("toolbox").Elements ("handtool")select tool.Value.ToUpper();
上述查询,第一次调用的 Elements
方法绑定的是 XContainer
的实例方法,而第二次 Elements
方法则绑定到了扩展方法上。
Eureka
Nodes
方法的返回值类型是IEnumerable<XNode>
,Elements
方法的返回值是IEnumerable<XElements>
,而 LINQ 的Elements
方法不支持IEnumerable<XNode>
,因此无法对Nodes
使用Elements
方法。
10.4.1.3 检索单个元素(element)
Element()
方法等价于 LINQ 中的 FirstOrDefault
,返回单层子节点匹配到的第一个元素,若元素不存在,返回 null。
Tips
Element("xyz").Value
调用在 xyz 元素不存在时将 抛出 NullReferenceException
。XElement
为string
类型定义了显式转换,可以通过强制类型避免此异常。即:string xyz = (string)settings.Element ("xyz");
当然,我们也可以使用
?.
10.4.1.4 获取子元素:Descendants
和 DescendantNodes
XContainer
提供了 Descendants
方法和 DescendantNodes
方法,用于访问全部子元素(Element)或全部子节点(Node)(以至整棵树)。
Descendants
方法可以接收一个元素名称,返回所有子元素 (XElement
对象)。
DescendantNodes
方法不接收参数,返回所有类型的子节点(包括 XText
)。
以如下代码为例,输出内容如下:
/*输出 XElement 元素
<toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>
<handtool>Hammer</handtool>
<handtool>Rasp</handtool>
<toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox>
<handtool>Saw</handtool>
<powertool>Nailgun</powertool>
*/
foreach (var node in bench.Descendants())Console.WriteLine(node.ToString(SaveOptions.DisableFormatting));
/* 输出全部节点
<toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>
<handtool>Hammer</handtool>
Hammer
<handtool>Rasp</handtool>
Rasp
<toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox>
<handtool>Saw</handtool>
Saw
<powertool>Nailgun</powertool>
Nailgun
<!--Be careful with the nailgun-->
*/
foreach (XNode node in bench.DescendantNodes())Console.WriteLine (node.ToString (SaveOptions.DisableFormatting));
10.4.2 导航至父节点
XNode
及其子类(XDocument
除外)可以使用 AncestorXXX
方法导航至父节点,父节点的类型必然是 XElement
。
C 12 in a Nutshell The Definitive Reference.pdf - p550 - C 12 in a Nutshell The Definitive Reference-P550-20240427212207-l2o5u2p
Ancestors
返回一个序列,第一个元素是 Parent
,第二个元素是 Parent.Parent
,直至根元素。
Tips
XDocument
不是任何节点的父节点,但是任何XObject
都可以通过 Document
属性访问XDocument
。
Tips
可以使用 LINQ 查询根元素:
var root = bench.AncestorsAndSelf().Last();
上述代码不使用
Ancestors
方法,是因为bench
本身可能就是根节点。
如果存在
XDocument
,也可以通过 XObject.Document.Root
属性获取根节点。
10.4.3 导航至同级节点
C 12 in a Nutshell The Definitive Reference.pdf - p551 - C 12 in a Nutshell The Definitive Reference-P551-20240427214323-j9cnmbw
可以像链表一样使用 PreviousNode
和 NextNode
属性遍历节点。
Extra
事实上,节点在内部确实是以(单)链表的方式存储,因此
PreviousNode
属性的效率较低。
10.4.4 导航至节点的 Attribute
C 12 in a Nutshell The Definitive Reference.pdf - p551 - C 12 in a Nutshell The Definitive Reference-P551-20240427214634-4we23m4
Attribute
方法接受 name
参数,返回 0~1 个元素的序列(一个 XML 元素不能包含同名 Attribute)。
Tips
上述是
XElement
中的方法,XAttribute
类型还提供了Parent
属性、PreviousAttribute
和NextAttribute
属性。
10.5 更新 X-DOM
10.5.1 简单的值更新
C 12 in a Nutshell The Definitive Reference.pdf - p552 - C 12 in a Nutshell The Definitive Reference-P552-20240427215207-4o1xolr
SetValue
方法和 Value
属性用于替换/设置 Element 或 Attribute 的当前值。SetValue
方法接受 object
类型的数据,Value
属性仅接受 string
类型的数据。
二者赋值时,新值将替换所有子节点。
Tips
SetValue
方法内部实际调用的也是Value
属性。
10.5.2 更新子节点(Node)和 Attribute
C 12 in a Nutshell The Definitive Reference.pdf - p552 - C 12 in a Nutshell The Definitive Reference-P552-20240427220005-v9s23fd
上述方法都用于更新当前节点。
10.5.2.1 SetElementValue
方法和 SetAttributeValue
方法
这两个方法将自动实例化 XElement
/XAttribute
对象,并作为 子 元素添加至 当前 元素中,若有同名 Element/Attribute 则进行覆盖:
XElement settings = new XElement ("settings");settings.SetElementValue ("timeout", 30);
settings.SetElementValue ("timeout", 60);
<settings><timeout>30</timeout>
</settings><settings><timeout>60</timeout>
</settings>
10.5.2.2 Add
方法和 AddFirst
方法
Add
方法向内部节点的队尾插入节点; AddFirst
向内部节点的排头插入节点。
Tips
Add
方法定义在XContainer
中,AddAfterSelf
定义在XNode
中。
10.5.2.3 RemoveNodes
方法、 RemoveAttributes
方法和 RemoveAll
方法
RemoveNodes
方法用于移除持有的全部节点, RemoveAttributes
方法用于移除持有的全部 Attribute。 RemoveAll
可以一次性将二者全部移除。
10.5.2.4 ReplaceXXX
方法
等价于 RemoveXXX
方法 + Add
方法。
10.5.3 通过父节点更新子节点
C 12 in a Nutshell The Definitive Reference.pdf - p553 - C 12 in a Nutshell The Definitive Reference-P553-20240428123530-3pytrve
上述方法操作的是当前节点的父节点(Parent
),因此父节点不能为 null 。
AddBeforeSelf
方法和 AddAfterSelf
方法
用于在当前节点的前、后插入其他节点。
Remove
方法
用于在父节点中移除当前节点。
ReplaceWith
方法
用于在父节点中替换当前节点
10.5.3.1 移除节点或属性序列(LINQ)
System.Xml.Linq
提供了一系列扩展方法用于从父节点移除元素。后续代码对应的 XML 如下:
<contacts><customer name="Mary" /><customer name="Chris" archived="true" /><supplier name="Susan"><phone archived="true">012345678<!--confidential--></phone></supplier>
</contacts>
Elements().Remove()
从10.4.1.2 检索 elements可知,Elements
方法返回的是单层子节点,因此如下代码只会移除当前层的子节点:
contacts.Elements().Where (e => (bool?) e.Attribute ("archived") == true).Remove();
<contacts><customer name="Mary" /><supplier name="Susan"><phone archived="true">012345678<!--confidential--></phone></supplier>
</contacts>
Descendants().Remove()
从10.4.1.4 获取子元素:Descendants 和 DescendantNodes可知,Descendants
方法返回所有层次的子节点,因此如下代码会移除任何匹配到的子节点:
contacts.Descendants().Where (e => (bool?) e.Attribute ("archived") == true).Remove();
<contacts><customer name="Mary" /><supplier name="Susan" />
</contacts>
综合使用
以下代码移除了注释为“confidential”的联系人:
contacts.Elements().Where (e => e.DescendantNodes().OfType<XComment>().Any (c => c.Value == "confidential")).Remove();
<contacts><customer name="Mary" /><customer name="Chris" archived="true" />
</contacts>
10.6 使用 Value
10.6.1 设置 Value
如10.5.1 简单的值更新所述:
SetValue
方法和Value
属性用于替换/设置 Element 或 Attribute 的当前值。SetValue
方法接受 object
类型的数据,Value
属性仅接受 string
类型的数据。
Warn
通过
Value
设置值时,DataTime
要使用 XmlConvert
转化数据。
SetValue
和XElement
/XAttribute
的构造器会自动调用 XmlConvert
对数据格式化,保证了数据格式的正确性。
10.6.2 获得 Value
XElement
/XAttribute
内部定义了诸多显式转换(如下类型),因此可以直接通过自定义转换获取 Value。
- 标准数值类型
-
string
、bool
、DateTime(Offset)
、TimeSpan
、Guid
- 上述值类型的
Nullable<>
版本。
XElement e = new XElement ("now", DateTime.Now);
DateTime dt = (DateTime) e;XAttribute a = new XAttribute ("resolution", 1.234);
double res = (double) a;
Suggestion
XML 的元素和 Attribute 不会记录数据的原始类型,上述显式转换可能执行失败。推荐将代码包裹在 try/catch 块中,并捕获
FormatException
异常。
10.6.2.1 XML 对象与空运算符
Element
方法和 Attribute
方法的返回值非常适合转化为 Nullable<>
类型,以如下代码为例,程序不会因为“timeout”不存在而抛出异常:
int timeout1 = (int) x.Element ("timeout");
int? timeout2 = (int?) x.Element ("timeout");
配合空合并运算(??
)可以去除最终结果中的可空类型。如下代码在 resolution
属性不存在时返回 1.0:
double resolution = (double?) x.Attribute ("resolution") ?? 1.0;
10.6.3 值与混合内容节点
XML 是允许混合内容的,形式如下:
<summary>An XAttribute is <bold>not</bold> an XNode</summary>
要得到上述 X-DOM,需通过 XText
节点:
XElement summary =new XElement ("summary",new XText ("An XAttribute is "),new XElement ("bold", "not"),new XText (" an XNode"));
<!--输出-->
<summary>An XAttribute is <bold>not</bold> an XNode</summary>
其中 summary
的 Value
如下,它拼接了各个子节点的 Value
:
An XAttribute is not an XNode
Tips
实际传入 string 也是可以的,构造器内部会隐式转为
XText
:XElement summary =new XElement ("summary","An XAttribute is ",new XElement ("bold", "not")," an XNode");
10.6.4 自动连接 XText
节点
向 XElement
中添加简单内容(字符串)时,X-DOM 会将内容附加至现有 XText
:
// 1 个 XText节点
var e1 = new XElement ("test", "Hello"); e1.Add ("World");
e1.Nodes().Count().Dump (); // 输出 1
// 1 个 XText节点
var e2 = new XElement ("test", "Hello", "World");
e2.Nodes().Count().Dump (); // 输出 1
如果显式创建、添加 XText
节点,则会得到多个子节点:
// 2 个 XText节点
var e3 = new XElement ("test", new XText ("Hello"), new XText ("World"));
e3.Nodes().Count().Dump (); // 输出 2
XElement
不会连接这两个 XText
节点,节点对象的标识均得到保留。即便如此,其 ToString
输出的内容仍是拼接的:
<test>HelloWorld</test>
10.7 文档和声明
10.7.1 XDocument
XDocument
可接受的内容包括:
XElement |
XDeclaration |
XDocumentType |
XProgressingInstruction |
XComment |
|
---|---|---|---|---|---|
数量 | 1 | 1 | 1 | 多个 | 多个 |
是否必选 | 是 | 否 | 否 | 否 | 否 |
其中 XElement
作为 X-DOM 的根节点。
若 XDocument
未定义 XDeclaration
,调用 XDocument.Save
时,会自动添加默认的 XML 声明:
// 未定义 XDeclaration
var value = new XDocument (new XElement("test", "data")
);
<!--Save 方法生成的内容:-->
<?xml version="1.0" encoding="utf-16"?>
<test>data</test>
10.7.2 XML 声明(declaration)
10.7.2.1 XML 声明的作用
XDeclaration
对象主要用于指导 XML 的序列化进程,影响的内容有二:
- 文本编码标准
- 声明中的 encoding 和 standalone 如何定义
XDeclaration
构造器接受三个参数:version、encoding 和 standalone。
ExtraNotice
XML 写入器(writer)会忽略指定的 version 信息,总是写入“1.0”。
XML 声明中的编码方式必须使用 IETF 编码方式书写,例如“utf-16”。
10.7.2.2 XElement
和 XDocument
遵循的声明规则
XML 声明用于保证文件被阅读器(reader)正确解析(parse)并理解。XElement
和 XDocument
都遵循以下声明规则:
- 调用
Save
方法将内容写入文件,总是 会自动 写入 XML 声明。 - 调用
Save
方法将内容写入XmlWriter
时,除非XmlWriter
特别指定,否则 会 写入 XML 声明。 -
ToString
方法 不会 生成XML
声明。
Tips
如果不想让
XmlWriter
生成 XML 声明,可以设置XmlWriterSettings
对象的OmitXmlDeclaration
和ConformanceLevel
属性。另见11.2.0 XmlWriterSettings
Notice
XNode
的WriteTo
方法向XmlWriter
写入, 也会 添加 XML 声明。
10.7.2.3 将 XML 声明输出为字符串
若要将 XDocument
序列化为 string
,且包含声明,需使用 Save
方法:
var doc =new XDocument (new XDeclaration ("1.0", "utf-8", "yes"),new XElement ("test", "data"));var output = new StringBuilder();
var settings = new XmlWriterSettings { Indent = true };using (XmlWriter xw = XmlWriter.Create (output, settings))doc.Save (xw);
<?xml version="1.0" encoding="utf-16" standalone="yes"?>
<test>data</test>
Warn
上述代码即使我们设置编码格式为 utf-8,实际输出的是 utf-16。因为我们输出的对象是
StringBuilder
,编码必然是 utf-16。XmlWriter
会自动判断实际输出编码格式,这有效避免了编码格式错误导致的异常。正因此,为避免输出错误的编码格式,
XDocument.ToString
不会包含XDeclaration
内容:var doc =new XDocument (new XDeclaration ("1.0", "utf-8", "yes"),new XElement ("test", "data")); doc.ToString().Dump();
输出: <test>data</test>
10.8 名称(Name)和 命名空间(namespace)
XML 的 namespace 用于避免 命名 冲突。例如 nil 可能有多种含义,但在 http://www.w3.org/2001/xmlschema-instance 命名空间下,表示 C# 中的 null。
10.8.1 XML 中的命名空间
XML 中的 namespace 通过 Attribute 声明:
<customer xmlns="http://domain.com/xmlspace"><address><postcode>02138</postcode></address>
</customer>
上述 XML 中,address 和 postcode 也 属于 http://domain.com/xmlspace
命名空间。若不希望子节点继承父节点的命名空间,需显式的令子节点 namespace 为 空 :
<customer xmlns="http://domain.com/xmlspace"><address xmlns=""><postcode>02138</postcode></address>
</customer>
当然,我们也可以按照10.8.1.1 前缀(namespace 别名)中的方式,为父节点分配前缀。
Info
关于专门设为空的 namespace,我仅在 XAML 中见过一次这样的应用。见x:XData
10.8.1.1 前缀(namespace 别名)
以如下 XML 为例,一次性完成了两步操作(定义和使用):
-
xmlns:nut
定义了前缀 nut; -
nut:customer
将前缀分配至 当前 元素。
<nut:customer xmlns:nut="http://domain.com/xmlspace"/>
Notice
拥有前缀的元素,它的子元素 不会 自动使用相同的 namespace。在如下 XML 中,firstname 的 namespace 分别为 空 和 nut :
<nut:customer xmlns:nut="http://domain.com/xmlspace"><firstname>Joe</firstname> </nut:customer>
<nut:customer xmlns:nut="http://domain.com/xmlspace"><nut:firstname>Joe</nut:firstname> </customer>
在 XAML 中我们会同时引入多个 namespace,此时可以通过前缀区分不同 namespace 下的成员:
<Window x:Class="WpfApp1.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:WpfApp1"mc:Ignorable="d"Title="MainWindow" Height="450" Width="800"><Grid></Grid> </Window>
10.8.1.2 Attribute 与 namespace
XML 中的 Attribute 若要标记 namespace,必须通过前缀。如:
<customer xmlns:nut="OReilly.Nutshell.CSharp" nut:id="123" />
Warn
未用前缀限定的 Attribute 默认使用 空 的 namespace,它不从父元素继承默认 namespace。
一般来说,Attribute 是元素的本地特征,不需要 namespace。通用 Attribute、元数据 Attribute 例外,譬如之前提到的 W3C 中的 nil 代表了 C# 中的 null:
<customer xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><firstname>Joe</firstname><lastname xsi:nil="true" />
</customer>
10.8.2 在 X-DOM 中指定 namespace
为 X-DOM 添加 namespace 的方法有二:
本地名称前用 大括号 指定
以如下代码为例:
new XElement ("{http://domain.com/xmlspace}customer", new XAttribute("{http://domain.com/xmlspace}id", "123"),"Bloggs"
);
<customer p1:id="123"xmlns:p1="http://domain.com/xmlspace"xmlns="http://domain.com/xmlspace">Bloggs
</customer>
使用 XNamespace
使用方式如下:
XNamespace ns = "http://domain.com/xmlspace";
new XElement(ns + "data",new XAttribute(ns + "id", 456),"123"
);
<data p1:id="123"xmlns:p1="http://domain.com/xmlspace"xmlns="http://domain.com/xmlspace">123
</data>
XNamespace
和 XName
都定义了与 string
类型的隐式转换;XNamespace
还重载了 +
运算符,返回类型为 XName
。
X-DOM 的所有构造器和方法,都使用 XName
类型作为 Element/Attribute 的名称参数,因此我们可以使用 XNamespace + string
的方式传入参数。
10.8.3 X-DOM 和默认 namespace
在 X-DOM 中,不存在“继承 namespace”的概念,若想继承父项 namespace,每个成员都需要 显式指定 。而 X-DOM 在读取或输出 XML 时,若父子 namespace 相同,将 自动缺省子项的 namespace :
XNamespace ns = "http://domain.com/xmlspace";var data =new XElement (ns + "data",new XElement (ns + "customer", "Bloggs"),new XElement (ns + "purchase", "Bicycle"));
<data xmlns="http://domain.com/xmlspace"><customer>Bloggs</customer><purchase>Bicycle</purchase>
</data>
若父项指定了 namespace,子项未指定,子项的 namespace 会标记为 空 :
XNamespace ns = "http://domain.com/xmlspace";var data =new XElement (ns + "data",new XElement ("customer", "Bloggs"),new XElement ("purchase", "Bicycle"));
<data xmlns="http://domain.com/xmlspace"><customer xmlns="">Bloggs</customer><purchase xmlns="">Bicycle</purchase>
</data>
Warn
当成员的 namespace 不为 空 ,查找元素时传入的 Name 需包含 namespace 信息,例如:
XElement x = data.Element (ns + "customer"); // OK XElement y = data.Element ("customer"); // null
Suggest
上述指定 namespace 的方式显然很麻烦,我们可以在后期统一指定 namespace:
foreach (XElement e in data.DescendantsAndSelf())if (e.Name.Namespace == "")e.Name = ns + e.Name.LocalName;
10.8.4 添加前缀
namespace 在 XML 中本质是 Attribute ,因此我们可以通过 XAttribute
为成员添加前缀。该 Attribute 的 Name
为 XNamespace.Xmlns + 别名
,Value
为对应的 namespace。以如下 X-DOM 为例:
<data xmlns="http://domain.com/space1"><element xmlns="http://domain.com/space2">value</element><element xmlns="http://domain.com/space2">value</element><element xmlns="http://domain.com/space2">value</element>
</data>
插入前缀方式为:
<ns1:data xmlns:ns1="http://domain.com/space1" xmlns:ns2="http://domain.com/space2"><ns2:element>value</ns2:element><ns2:element>value</ns2:element><ns2:element>value</ns2:element>
</ns1:data>
XNamespace ns1 = "http://domain.com/space1";
XNamespace ns2 = "http://domain.com/space2";var mix =new XElement (ns1 + "data",new XElement (ns2 + "element", "value"),new XElement (ns2 + "element", "value"),new XElement (ns2 + "element", "value"));
// 插入 namespace
mix.SetAttributeValue (XNamespace.Xmlns + "ns1", ns1);
mix.SetAttributeValue (XNamespace.Xmlns + "ns2", ns2);
// 或
mix.Add(new XAttribute(XNamespace.Xmlns + "ns1", ns1));
mix.Add(new XAttribute(XNamespace.Xmlns + "ns2", ns2));
前缀对于 Attribute 同样有效:
XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance";
var nil = new XAttribute (xsi + "nil", true);var cust =new XElement ("customers",//new XAttribute (XNamespace.Xmlns + "xsi", xsi),new XElement ("customer",new XElement ("lastname", "Bloggs"),new XElement ("dob", nil),new XElement ("credit", nil)));
cust.SetAttributeValue(XNamespace.Xmlns + "xsi", xsi);
<customers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><customer><lastname>Bloggs</lastname><dob xsi:nil="true" /><credit xsi:nil="true" /></customer>
</customers>
Tips
前缀的引入不会影响 X-DOM 内部的实际结构,它仅在输入、输出时才会用到(如序列化和反序列化)。
Error
虽然 namespace 的声明方式和 XAttribute 极其相似,但不可以用如下方式声明:
var mix = new XElement("data", new XAttribute("xmlns", "{http://domain.com/space1}"));
xmlns
是 xml 中的特殊关键字,上述代码mix
在使用时将抛出XmlException
异常。
xmlns
特性是 XML 中的一个特殊 特性 ,专门用于声明 namespace。
10.9 注解(Annotations)
注解用于存放私有数据,可以附加在任何的 XObject
上,X-DOM 将其视为黑盒。有如下方法操作注解对象:
// 添加或移除
public void AddAnnotation (object annotation)
public void RemoveAnnotations<T>() where T : class
// 检索
public T Annotation<T>() where T : class
public IEnumerable<T> Annotations<T>() where T : class
public T Annotation<T>() where T : class
public IEnumerable<T> Annotations<T>() where T : class
注解使用 Type 作为键(必须是引用类型)。用法如下:
XElement e = new XElement ("test");e.AddAnnotation (new CustomData { Message = "Hello" } );
e.Annotations<CustomData>().First().Message.Dump();e.RemoveAnnotations<CustomData>();
e.Annotations<CustomData>().Count().Dump();class CustomData { internal string Message; }
Error
在10.3.3 自动深度克隆(Automatic Deep Cloning)中我们提到,
XObject
如果有父项,该节点赋值给其他父项时会进行深拷贝。但注解不参与该拷贝,它所在的节点进行拷贝时,新节点的注解为空。
10.10 将数据映射到 X-DOM #delay# 用不到,看不懂,剩余内容推迟再看
我们可以使用 LINQ 将数据从数据源映射至 X-DOM 中,只要该数据源支持 LINQ 查询。
例如我们要通过LINQ查询得到形如下方的 XML:
<customers><customer id="1"><name>Sue</name><buys>3</buys></customer>...
</customers>
var customers =new XElement ("customers",new XElement ("customer", new XAttribute ("id", 1),new XElement ("name", "Sue"),new XElement ("buys", 3))
);
在新版 EF 上的操作如下:
var customers =new XElement ("customers",from c in Customers.AsEnumerable()select new XElement ("customer", new XAttribute ("id", c.ID),new XElement ("name", c.Name),new XElement ("buys", c.Purchases.Count)));
// or
var sqlQuery =from c in Customers.AsEnumerable()select new XElement ("customer", new XAttribute ("id", c.ID),new XElement ("name", c.Name),new XElement ("buys", c.Purchases.Count));var customers = new XElement ("customers", sqlQuery);
<customers><customer id="1"><name>Tom</name><buys>3</buys></customer><customer id="2"><name>Harry</name><buys>2</buys></customer>...
</customers>