第10章 LINQ to XML

10.1 架构概述——DOM 和 LINQ to XML 的 DOM

XML 文档可以用一棵对象树完整的表示,这称为“文档对象模型(document object model)”

LINQ to XML 由两部分组成:

  1. XML DOM,简称为 X-DOM
  2. 大约 10 个查询运算符

LINQ 也可以用于查询 W3C 标准的旧 DOM,不过 X-DOM 对 LINQ 查询更为友好:

  1. X-DOM 部分方法可以返回 IEnumerable​ 序列;
  2. 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:

  1. 和 XML 自身结构相似;

  2. 它可以使用 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​ 将可选参数数组的所有对象都转为了 NodeAttribute ,其处理逻辑如下:

  1. 忽略 null 对象;
  2. XNode​、XStreamingElement​ 对象,添加至 Node 集合中;
  3. XAttribute​ 对象,添加至 Attribute 集合中;
  4. string​ 对象,包装成 XText ​ 节点,添加至 Node 集合中;
  5. IEnumerable ​ 对象,遍历所有内容,按照 1~4 步处理;
  6. 其他:将对象转化为 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 导航至子节点

mindmap
子节点
单一子节点
FirstNode
LastNode
Element
单层子节点
Nodes
Elements
深层子节点
Decendants
DecendantNodes

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 元素不存在时将 抛出 NullReferenceExceptionXElement​ 为 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

mindmap
更新子节点
XNode
AddBeforeSelf
AddAfterSelf
Remove
ReplaceWith
XContainer
Add
AddFirst
RemoveNodes
ReplaceNodes
XElement
RemoveAttributes
RemoveAll
ReplaceAttributes
ReplaceAll
Value 属性
SetValue
SetElementValue
SetAttributeValue
XAttribute
Value 属性
SetValue
Remove
LINQ
Elements.Remove
Decendents.Remove

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。

  1. 标准数值类型
  2. string​、bool​、DateTime(Offset)​、TimeSpan​、Guid
  3. 上述值类型的 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 的序列化进程,影响的内容有二:

  1. 文本编码标准
  2. 声明中的 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​ 都遵循以下声明规则:

  1. 调用 Save​ 方法将内容写入文件,总是 会自动 写入 XML 声明。
  2. 调用 Save​ 方法将内容写入 XmlWriter​ 时,除非 XmlWriter​ 特别指定,否则 写入 XML 声明。
  3. 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 为例,一次性完成了两步操作(定义和使用):

  1. xmlns:nut​ 定义了前缀 nut;
  2. 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>

第10章 LINQ to XML的更多相关文章

  1. 24.C#LINQ TO XML(十二章12.3)

    自己也写了那么多,但还有很多不懂,有点浮躁吧,但饭还是要吃啊,说说LINQ TO XML吧. LINQ TO XML位于System.Xml.Linq程序集,并且大多数类型位于System.Xml.L ...

  2. XML操作:2.LINQ TO XML(http://www.cnblogs.com/AlexLiu/archive/2008/10/27/linq.html)

    LINQ to XML 建立,读取,增,删,改   LINQ to XML的出现使得我们再也不需要使用XMLDocument这样复杂的一个个的没有层次感的添加和删除.LINQ可以使的生成的XML文档在 ...

  3. LINQ系列:LINQ to XML类

    LINQ to XML由System.Xml.Linq namespace实现,该namespace包含处理XML时用到的所有类.在使用LINQ to XML时需要添加System.Xml.Linq. ...

  4. LINQ系列:LINQ to XML操作

    LINQ to XML操作XML文件的方法,如创建XML文件.添加新的元素到XML文件中.修改XML文件中的元素.删除XML文件中的元素等. 1. 创建XML文件 string xmlFilePath ...

  5. LINQ系列:LINQ to XML查询

    1. 读取XML文件 XDocument和XElement类都提供了导入XML文件的Load()方法,可以读取XML文件的内容,并转换为XDocument或XElement类的实例. 示例XML文件: ...

  6. Linq对XML的简单操作

    前两章介绍了关于Linq创建.解析SOAP格式的XML,在实际运用中,可能会对xml进行一些其它的操作,比如基础的增删该查,而操作对象首先需要获取对象,针对于DOM操作来说,Linq确实方便了不少,如 ...

  7. C#学习之Linq to Xml

    前言 我相信很多从事.NET开发的,在.NET 3.5之前操作XML会比较麻烦,但是在此之后出现了Linq to Xml,而今天的主人公就是Linq to Xml,废话不多说,直接进入主题. 题外:最 ...

  8. C#中的Linq to Xml详解

    这篇文章主要介绍了C#中的Linq to Xml详解,本文给出转换步骤以及大量实例,讲解了生成xml.查询并修改xml.监听xml事件.处理xml流等内容,需要的朋友可以参考下 一.生成Xml 为了能 ...

  9. LINQ TO XML 个人的一些心得1

    最近没事做,刚来到一个新公司.写了一些处理xml的项目  就是把一些xml的数据处理后存储到数据库中.原本还是准备用原来的xml来写的.在群里有个人说,用linq to xml 好了,比较快捷.就看了 ...

  10. 高性能Linux服务器 第10章 基于Linux服务器的性能分析与优化

    高性能Linux服务器 第10章    基于Linux服务器的性能分析与优化 作为一名Linux系统管理员,最主要的工作是优化系统配置,使应用在系统上以最优的状态运行.但硬件问题.软件问题.网络环境等 ...

随机推荐

  1. VUE懒加载的table前端搜索

    // 前端搜索 fliterData() { const search = this.search if (search) { this.blist = this.list.filter(item = ...

  2. Litctf2024-郑州轻工业大学第二届ctf-校内赛道wp

    战队:怎落笔都不对 最终成绩校内第4 MISC 1. 盯帧珍珠 打开文件发现是一个图片,放入 010 查看得文件头是 gif 格式 改为gif后缀得到一个GIF图,在下面这个网站分解,即可得到flag ...

  3. Java8提供的Stream方式进行分组GroupingBy

    有时我们需要对集合进行分组操作,这时可以使用Java8提供的Stream方式进行分组.挺好用的,此处记录下.直接贴code:   Road实体: @Data @NoArgsConstructor @A ...

  4. 编译器-FOLLOW集合

    语法分析器的两个重要函数 FIRST和FOLLOW 一.FOLLOW的定义 在句型中紧跟在A右边的终结符号的集合 如果A是某些句型的最右符号,那么$在FOLLOW(A)中 A:非终结符 二.计算方法 ...

  5. nginx的子路径重写替换

    ​在nginx中配置proxy_pass代理转发时,如果在proxy_pass后面的url加/,表示绝对根路径:如果没有/,表示相对路径,把匹配的路径部分也给代理走. 假设下面四种情况分别用 http ...

  6. mysql5.7以后group by 报错 sql_mode=only_full_group_by的解决方法

    一.发现问题 1.查询语句 SELECT * from class group by class_name; 2.报错结果 ..... this is incompatible with sql_mo ...

  7. k8s calico-node错误日志 listen tcp: lookup localhost on 8.8.4.4:53: no such host

    项目场景:K8s搭建 问题描述:查看pods状态,发现 calico-node异常[root@k8s-master ~]# kubectl get pods --all-namespacesNAMES ...

  8. 龙哥量化:通达信的macd改进优化方法及选股公式源码

    有很多同学是看macd的数值,遇到股价比较低的,macd数值变成0.00,就看不明白了, 优化: 第一步,给股价乘100,所有的哦 源码: DIF:EMA(CLOSE*100,12)-EMA(CLOS ...

  9. 在不同操作系统上安装 PostgreSQL

    title: 在不同操作系统上安装 PostgreSQL date: 2024/12/26 updated: 2024/12/26 author: cmdragon excerpt: PostgreS ...

  10. 如何使用图片的exif信息计算相机焦距

    135胶卷源于35mm高度的打孔电影胶片,1913年,德国人奥斯卡·巴纳克将其用于他发明的徕卡(Leica)牌小型照相机上,由此形成标准.35mm电影胶卷,35mm指的是胶卷的高度为35mm,由于上下 ...