龙空技术网

c# 10 教程:10 LINQ to XML

启辰8 190

前言:

现时兄弟们对“htmlobject层级”大概比较着重,姐妹们都想要剖析一些“htmlobject层级”的相关内容。那么小编在网络上汇集了一些关于“htmlobject层级””的相关内容,希望同学们能喜欢,咱们一起来学习一下吧!

.NET 提供了许多用于处理 XML 数据的 API。通用 XML 文档处理的主要选择是 LINQ to XML 包含一个轻量级、对 LINQ 友好的 XML 文档对象模型,以及一组补充查询运算符。

在本章中,我们将完全专注于 LINQ to XML。在第 中,我们将介绍只进 XML 读取器/编写器,在中,我们将介绍用于处理模式和样式表的类型。.NET 还包括基于 XmlDocument 的旧版 DOM,我们不涉及。

注意

LINQ to XML 文档对象模型 (DOM) 设计得非常好,性能也很高。即使没有 LINQ,LINQ to XML DOM 作为低级 XmlReader 和 XmlWriter 类的轻量级外观也很有价值。

所有 LINQ to XML 类型都在 System.Xml.Linq 命名空间中定义。

体系结构概述

本节首先简要介绍 DOM 的概念,然后解释 LINQ to XML 的 DOM 背后的基本原理。

什么是 DOM?

请考虑以下 XML 文件:

<?xml version="1.0" encoding="utf-8"?><customer id="123" status="archived">  <firstname>Joe</firstname>  <lastname>Bloggs</lastname></customer>

与所有XML文件一样,我们从开始,然后是一个根,其名称是 客户 。客户元素有两个属性,每个都有一个名称(ID和状态)和值(“123”和“存档”)。在客户中,有两个子元素,名字和姓氏,每个都有简单的文本内容(“Joe”和“Bloggs”)。

其中每个构造(声明、元素、属性、值和文本内容)都可以用类表示。如果这些类具有用于存储子内容的集合属性,我们可以组装一个对象来完全描述文档。这称为或 DOM。

The LINQ to XML DOM

LINQ to XML 包含两件事:

一个XML DOM,我们称之为一组大约 10 个补充查询运算符

正如你所料,X-DOM由诸如XDocument,XElement和XAttribute 等类型组成。有趣的是,X-DOM 类型并不绑定到 LINQ,您可以加载、实例化、更新和保存 X-DOM,而无需编写 LINQ 查询。

相反,您可以使用 LINQ 查询由较旧的 W3C 兼容类型创建的 DOM。但是,这将是令人沮丧和限制的。X-DOM 的显著特点是它,这意味着:

它具有发出有用的 IEnumerable 序列的方法,您可以查询这些序列。它的构造函数设计为可以通过 LINQ 投影生成 X-DOM 树。X-DOM 概述

类型。这些类型中最常用的是 XElement . XObject 是层次结构的根;XElement和XDocument是层次结构的根源。

核心 X-DOM 类型

显示了从以下代码创建的 X-DOM 树:

string xml = @"<customer id='123' status='archived'>                 <firstname>Joe</firstname>                 <lastname>Bloggs<!--nice name--></lastname>               </customer>";XElement customer = XElement.Parse (xml);

一个简单的 X-DOM 树

XObject 是所有 XML 内容的抽象基类。它定义了指向容器运输树中的父元素的链接以及可选的 XDocument 。

XNode 是大多数 XML 内容(不包括属性)的基类。XNode 的显著特点是它可以位于混合型 XNode 的有序集合中。例如,请考虑以下 XML:

<data>  Hello world  <subelement1/>  <!--comment-->  <subelement2/></data>

在父元素<data>中,首先有一个XText节点(Hello world),然后是一个XElement节点,然后是一个XComment节点,然后是第二个XElement节点。相反,一个 XAttribute 将只容忍其他 XAttribute 作为对等方。

虽然XNode可以访问它的父XElement,但它没有子节点的概念:这是它的类XContainer的工作。XContainer定义了处理子级的成员,是XElement和XDocument的抽象基类。

XElement 引入了用于管理属性的成员,以及名称和值。在具有单个 XText 子节点的元素(相当常见的)情况下,XElement 上的 Value 属性封装了此子节点的内容以进行获取和设置操作,从而减少了不必要的导航。多亏了 Value ,您基本上可以避免直接使用 XText 节点。

XDocument 表示 XML 树的根。更准确地说,它了根XElement,添加了XDeclaration,处理指令和其他根级“绒毛”。与 W3C DOM 不同,它的使用是可选的:您可以加载、操作和保存 X-DOM,而无需创建 XDocument!对 XDocument 的不依赖还意味着您可以高效、轻松地将节点子树移动到另一个 X-DOM 层次结构。

加载和解析

XElement 和 XDocument 都提供静态加载和解析方法来从现有源代码构建 X-DOM 树:

Load 从文件、URI、Stream 、TextReader 或 XmlReader 构建 X-DOM。Parse 从字符串构建一个 X-DOM。

例如:

XDocument fromWeb = XDocument.Load (";);XElement fromFile = XElement.Load (@"e:\media\somefile.xml");XElement config = XElement.Parse (@"<configuration>    <client enabled='true'>      <timeout>30</timeout>    </client>  </configuration>");

在后面的部分中,我们将介绍如何遍历和更新 X-DOM。作为快速预览,以下是操作我们刚刚填充的配置元素的方法:

foreach (XElement child in config.Elements())  Console.WriteLine (child.Name);                     // clientXElement client = config.Element ("client");bool enabled = (bool) client.Attribute ("enabled");   // Read attributeConsole.WriteLine (enabled);                          // Trueclient.Attribute ("enabled").SetValue (!enabled);     // Update attributeint timeout = (int) client.Element ("timeout");       // Read elementConsole.WriteLine (timeout);                          // 30client.Element ("timeout").SetValue (timeout * 2);    // Update elementclient.Add (new XElement ("retries", 3));             // Add new elememtConsole.WriteLine (config);         // Implicitly call config.ToString()

这是最后一个 Console.WriteLine 的结果:

<configuration>  <client enabled="false">    <timeout>60</timeout>    <retries>3</retries>  </client></configuration>
注意

XNode 还提供了一个静态 ReadFrom 方法,该方法从 XmlReader 实例化和填充任何类型的节点。与 Load 不同,它在读取一个(完整)节点后停止,因此您可以继续从 XmlReader 手动读取。

你也可以做相反的事情,使用XmlReader或XmlWriter通过其CreateReader和CreateWriter方法读取或写入XNode。

我们将在第 中描述 XML 读取器和编写器以及如何将它们与 X-DOM 一起使用。

保存和序列化

在任何节点上调用 ToString 都会将其内容转换为 XML 字符串 — 如我们刚刚看到的那样,使用换行符和缩进进行格式化。(可以通过在调用 ToString 时指定 SaveOptions.DisableFormat 来禁用换行符和缩进。

XElement和XDocument还提供了一个保存方法,该方法将X-DOM写入文件,Stream,TextWriter或XmlWriter。如果指定文件,则会自动写入 XML 声明。在XNode类中还定义了一个WriteTo方法,它只接受一个XmlWriter。

我们更详细地描述了保存在中时对 XML 声明的处理。

实例化 X-DOM

您可以使用 Load 或 Parse 方法构建 X-DOM 树,方法是手动实例化对象并通过 XContainer 的 Add 方法将它们添加到父对象。

要构造一个 XElement 和 XAttribute ,只需提供一个名称和值:

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);Console.WriteLine (customer.ToString());

结果如下:

<customer id="123">  <firstname>Joe</firstname>  <lastname>Bloggs<!--nice name--></lastname></customer>

构造 XElement 时,值是可选的 - 您可以只提供元素名称并在以后添加内容。请注意,当我们确实提供了一个值时,一个简单的字符串就足够了 - 我们不需要显式创建和添加 XText 子节点。X-DOM 会自动完成这项工作,因此您可以简单地处理“值”。

功能结构

在前面的示例中,很难从代码中收集 XML 结构。X-DOM 支持另一种实例化模式,称为函数式构造(来自编程)。使用函数构造,您可以在单个表达式中构建整个树:

XElement customer =  new XElement ("customer", new XAttribute ("id", 123),    new XElement ("firstname", "joe"),    new XElement ("lastname", "bloggs",      new XComment ("nice name")    )  );

这有两个好处。首先,代码类似于 XML 的形状。其次,它可以合并到 LINQ 查询的选择子句中。例如,以下查询从 EF Core 实体类投影到 X-DOM 中:

XElement query =  new XElement ("customers",    from c in dbContext.Customers.AsEnumerable()    select      new XElement ("customer", new XAttribute ("id", c.ID),        new XElement ("firstname", c.FirstName),        new XElement ("lastname", c.LastName,          new XComment ("nice name")        )      )  );

我们将在本章后面的中进一步探讨这一点。

指定内容

函数构造是可能的,因为XElement(和XDocument)的构造函数被重载以接受参数对象数组:

public XElement (XName name, params object[] content)

XContainer 中的 Add 方法也是如此:

public void Add (params object[] content)

因此,您可以在构建或追加 X-DOM 时指定任意数量的任何类型的子对象。这是有效的,因为算作合法内容。要了解如何操作,我们需要检查每个内容对象在内部的处理方式。以下是XContainer做出的决定,按顺序排列:

如果对象为 null ,则忽略它。如果对象基于 XNode 或 XStreamingElement ,则会按原样添加到 Nodes 集合中。如果对象是 XAttribute ,则会将其添加到属性集合中。如果对象是一个字符串,则将其包装在 XText 节点中并添加到节点中。1如果对象实现 IEnumerable ,则会枚举它,并且相同的规则将应用于每个元素。否则,对象将转换为字符串,包装在 XText 节点中,然后添加到节点中。2

所有内容最终都位于两个存储桶之一中: 节点或属性 .此外,任何对象都是有效内容,因为它始终可以最终在其上调用 ToString 并将其视为 XText 节点。

注意

在对任意类型调用 ToString 之前,XContainer 首先测试它是否为以下类型之一:

float, double, decimal, bool,DateTime, DateTimeOffset, TimeSpan

如果是这样,它将在 XmlConvert 帮助程序类上调用适当的类型化 ToString 方法,而不是在对象本身上调用 ToString。这可确保数据可循环访问并符合标准 XML 格式规则。

自动深度克隆

将节点或属性添加到元素(无论是通过函数构造还是 Add 方法)时,节点或属性的 Parent 属性将设置为该元素。一个节点只能有一个父元素:如果将一个已经父级的节点添加到第二个父节点,则该节点将自动。在以下示例中,每个客户都有一个单独的地址副本:

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";Console.WriteLine (  customer2.Element ("address").Element ("street").Value);   // Lawley St

这种自动复制使 X-DOM 对象实例化没有副作用,这是函数式编程的另一个标志。

导航和查询

如您所料,XNode 和 XContainer 类定义了遍历 X-DOM 树的方法和属性。但是,与传统的 DOM 不同,这些函数不返回实现 IList<T> 的集合。相反,它们返回单个值或实现 IEnumerable<T> 的,然后需要在其上执行 LINQ 查询(或使用 foreach 枚举)。这允许使用熟悉的 LINQ 查询语法进行高级查询以及简单的导航任务。

注意

元素和属性名称在 X-DOM 中区分大小写,就像它们在 XML 中一样。

子节点导航

返回类型

成员

作品

新德

FirstNode { get; }

XContainer

LastNode { get; }

XContainer

IEnumerable<XNode>

节点()

XContainer*

DescendantNodes()

XContainer*

DescendantNodesAndSelf()

XElement*

XElement

元素 (XName)

XContainer

IEnumerable<XElement>

元素()

XContainer*

元素 (XName)

XContainer*

后代()

XContainer*

后代(XName)

XContainer*

后代和自己()

XElement*

后裔和自己(XName)

XElement*

布尔

HasElements { get; }

XElement

注意

此表和其他表的第三列中标有星号的函数也对相同类型的进行操作。例如,您可以在 XContainer 或一系列 XContainer 对象上调用节点。这是可能的,因为 System.Xml.Linq 中定义了扩展方法,这是我们在概述中讨论的补充查询运算符。

FirstNode、LastNode 和 Node

FirstNode 和 LastNode 允许您直接访问第一个或最后一个子节点;子项作为序列返回。所有三个函数仅考虑直系:

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")            );foreach (XNode node in bench.Nodes())  Console.WriteLine (node.ToString (SaveOptions.DisableFormatting) + ".");

这是输出:

<toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>.<toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox>.<!--Be careful with the nailgun-->.
检索元素

元素方法仅返回类型为 XElement 的子节点:

foreach (XElement e in bench.Elements())  Console.WriteLine (e.Name + "=" + e.Value);    // toolbox=HammerRasp                                                 // toolbox=SawNailgun

以下 LINQ 查询查找带有射钉枪的工具箱:

IEnumerable<string> query =  from toolbox in bench.Elements()  where toolbox.Elements().Any (tool => tool.Value == "Nailgun")  select toolbox.Value;RESULT: { "SawNailgun" }

下一个示例使用 SelectMany 查询检索所有中的手部工具:

IEnumerable<string> query =  from toolbox in bench.Elements()  from tool in toolbox.Elements()  where tool.Name == "handtool"  select tool.Value;RESULT: { "Hammer", "Rasp", "Saw" }
注意

元素本身等效于节点上的 LINQ 查询。我们前面的查询可以按如下方式启动:

from toolbox in bench.Nodes().OfType<XElement>()where ...

元素也可以只返回给定名称的元素:

int x = bench.Elements ("toolbox").Count();    // 2

这等效于以下内容:

int x = bench.Elements().Where (e => e.Name == "toolbox").Count();  // 2

元素也被定义为接受 IEnumerable<XContainer 的扩展方法>或者更准确地说,它接受这种类型的参数:

IEnumerable<T> where T : XContainer

这也允许它处理元素序列。使用此方法,我们可以重写在所有工具箱中查找手动工具的查询,如下所示:

from tool in bench.Elements ("toolbox").Elements ("handtool")select tool.Value;

对元素的第一次调用绑定到XContainer的实例方法;对元素的第二次调用绑定到扩展方法。

检索单个元素

方法 Element (单数) 返回给定名称的第一个匹配元素。元素对于简单导航很有用,如下所示:

XElement settings = XElement.Load ("databaseSettings.xml");string cx = settings.Element ("database").Element ("connectString").Value;

Element 等效于调用 Elements(),然后应用带有名称匹配谓词的 LINQ 的 FirstOrDefault 查询运算符。如果请求的元素不存在,则元素返回 null。

注意

元素(“xyz”)。如果元素 xyz 不存在,值将引发 NullReferenceException。如果您更喜欢 null 而不是异常,请使用 null 条件运算符 — Element(“xyz”)?。值 - 或将 XElement 强制转换为字符串,而不是查询其 Value 属性。换句话说:

string xyz = (string) settings.Element ("xyz");

这是有效的,因为 XElement 定义了一个显式字符串转换 — 只是为此目的!

检索后代

XContainer 还提供了 Descendants 和 DescendantNode 方法,这些方法返回子元素或节点及其所有子元素,依此类推(整个树)。子体接受可选元素名称。回到我们之前的示例,我们可以使用后代来查找所有手动工具:

Console.WriteLine (bench.Descendants ("handtool").Count());  // 3

包括父节点和叶节点,如以下示例所示:

foreach (XNode node in bench.DescendantNodes())  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-->

下一个查询提取 X-DOM 中任何位置包含单词“小心”的所有注释:

IEnumerable<string> query =  from c in bench.DescendantNodes().OfType<XComment>()  where c.Value.Contains ("careful")  orderby c.Value  select c.Value;
家长导航

所有 XNode 都有一个父属性和用于父导航的方法。父母永远是XElement:AncestorXXX

返回类型

成员

作品

XElement

父 { get; }

新德

Enumerable<XElement>

祖先()

新德

祖先(XName)

新德

祖先和自己()

XElement

祖先和自己 (XName)

XElement

如果 x 是 XElement,则以下内容始终打印为 true:

foreach (XNode child in x.Nodes())  Console.WriteLine (child.Parent == x);

但是,如果 x 是 XDocument ,情况并非如此。XDocument很奇特:它可以有孩子,但永远不能成为任何人的父母!要访问 XDocument ,请改用 Document 属性;这适用于 X-DOM 树中的任何对象。

Ancestors 返回一个序列,其第一个元素是 Parent,下一个元素是 Parent.Parent ,依此类推,直到根元素。

注意

可以使用 LINQ 查询 AncestorsAndSelf() 导航到根元素。最后() .

实现相同目的的另一种方法是调用 Document.Root ,尽管这仅在存在 XDocument 时才有效。

对等节点导航

返回类型

成员

定义于

布尔

IsBefore (XNode node)

新德

IsAfter (XNode node)

新德

新德

PreviousNode { get; }

新德

NextNode { get; }

新德

IEnumerable<XNode>

NodesBeforeSelf()

新德

NodesAfterSelf()

新德

IEnumerable<XElement>

ElementsBeforeSelf()

新德

ElementsBeforeSelf (XName name)

新德

ElementsAfterSelf()

新德

ElementsAfterSelf (XName name)

新德

使用 PreviousNode 和 NextNode(以及 FirstNode / LastNode),您可以遍历节点,感觉像链表。这并非巧合:在内部,节点存储在链表中。

注意

XNode内部使用表,因此PreviousNode的性能不高。

属性导航

返回类型

成员

定义于

布尔

HasAttributes { get; }

XElement

X致敬

属性(XName 名称)

XElement

FirstAttribute { get; }

XElement

LastAttribute { get; }

XElement

IEnumerable<XAttribute>

属性()

XElement

属性(XName 名称)

XElement

此外,XAttribute 还定义了 PreviousAttribute 和 NextAttribute 属性以及 Parent 。

接受名称的属性方法返回具有零个或一个元素的序列;元素在 XML 中不能具有重复的属性名称。

更新 X-DOM

您可以通过以下方式更新元素和属性:

调用 SetValue 或重新分配 Value 属性。调用 SetElementValue 或 SetAttributeValue 。调用其中一个方法。RemoveXXX调用 or 方法之一,指定新鲜内容。AddXXXReplaceXXX

还可以重新分配 XElement 对象上的“名称”属性。

简单值更新

成员

作品

设置值(对象值)

Xelement,Xaettribute

值 { get; set }

Xelement,Xaettribute

方法将元素或属性的内容替换为简单值。设置 Value 属性执行相同的操作,但仅接受字符串数据。我们将在后面的中详细介绍这两个函数。

调用 SetValue(或重新赋值)的效果是它替换了所有子节点:

XElement settings = new XElement ("settings",                      new XElement ("timeout", 30)                    );settings.SetValue ("blah");Console.WriteLine (settings.ToString());  // <settings>blah</settings>
更新子节点和属性

类别

成员

作品

添加(参数对象[] 内容)

XContainer

添加优先 (参数对象[] 内容)

XContainer

删除

删除节点()

XContainer

删除属性()

XElement

全部删除()

XElement

更新

ReplaceNodes(params object[] content)

XContainer

替换属性(参数对象[] 内容)

XElement

ReplaceAll (params object[] content

XElement

SetElementValue (XName name, object value)

XElement

SetAttributeValue (XName name, object value)

XElement

该组中最方便的方法包括最后两种: SetElementValue 和 SetAttributeValue .它们用作实例化 XElement 或 的快捷方式,然后将其添加到父级,替换该名称的任何现有元素或属性:

XElement settings = new XElement ("settings");settings.SetElementValue ("timeout", 30);     // Adds child nodesettings.SetElementValue ("timeout", 60);     // Update it to 60

“添加”将子节点追加到元素或文档。AddFirst 执行相同的操作,但插入集合的开头而不是结尾。

您可以使用 删除节点 或 删除属性 一次性删除所有子节点或属性。RemoveAll 等效于调用这两个方法。

这些方法等效于删除 ing,然后添加 ing。他们拍摄输入的快照,因此e.ReplaceNodes(e.Nodes())按预期工作。ReplaceXXX

通过父级更新

成员

作品

AddBeforeSelf (params object[] content)

新德

AddAfterSelf (params object[] content)

新德

删除()

XNode , XAttribute

替换为(参数对象[] 内容)

新德

方法“AddBeforeSelf”、“AddAfterSelf”、“Remove ”和“替换与”不对节点的子节点进行操作。相反,它们对节点本身所在的集合进行操作。这要求节点具有父元素,否则将引发异常。AddBeforeSelf 和 AddAfterSelf 对于将节点插入任意位置很有用:

XElement items = new XElement ("items",                   new XElement ("one"),                   new XElement ("three")                 );items.FirstNode.AddAfterSelf (new XElement ("two"));

结果如下:

<items><one /><two /><three /></items>

插入到长元素序列中的任意位置是有效的,因为节点存储在链表内部。

Remove 方法从其父节点中删除当前节点。ReplaceWith 执行相同的操作,然后在同一位置插入一些其他内容:

XElement items = XElement.Parse ("<items><one/><two/><three/></items>");items.FirstNode.ReplaceWith (new XComment ("One was here"));

结果如下:

<items><!--one was here--><two /><three /></items>
删除一系列节点或属性

借助 System.Xml.Linq 中的扩展方法,您还可以在一系列节点或属性上调用 Delete。考虑一下这个X-DOM:

XElement contacts = XElement.Parse (@"<contacts>    <customer name='Mary'/>    <customer name='Chris' archived='true'/>    <supplier name='Susan'>      <phone archived='true'>012345678<!--confidential--></phone>    </supplier>  </contacts>");

以下内容将删除所有客户:

contacts.Elements ("customer").Remove();

以下内容将删除所有存档的联系人(因此消失):

contacts.Elements().Where (e => (bool?) e.Attribute ("archived") == true)                   .Remove();

如果我们将 Elements() 替换为 Descendants(),则整个 DOM 中的所有存档元素都将消失,从而产生以下结果:

<contacts>  <customer name="Mary" />  <supplier name="Susan" /></contacts>

下一个示例删除其树中任何位置带有注释“机密”的所有联系人:

contacts.Elements().Where (e => e.DescendantNodes()                                 .OfType<XComment>()                                 .Any (c => c.Value == "confidential")                          ).Remove();

结果如下:

<contacts>  <customer name="Mary" />  <customer name="Chris" archived="true" /></contacts>

与此形成对比的是以下更简单的查询,该查询从树中删除所有注释节点:

contacts.DescendantNodes().OfType<XComment>().Remove();
注意

在内部,Remove 方法首先将所有匹配的元素读入临时列表,然后枚举临时列表以执行删除。这可以防止同时删除和查询可能导致的错误。

使用值

XElement 和 XAttribute 都具有字符串类型的 Value 属性。如果一个元素有一个 XText 子节点,XElement 的 Value 属性将充当该节点内容的便捷快捷方式。对于 XAttribute ,Value 属性只是属性的值。

尽管存在存储差异,但 X-DOM 提供了一组一致的操作来处理元素和属性值。

设置值

有两种方法可以分配值:调用 SetValue 或分配 Value 属性。SetValue 更灵活,因为它不仅接受字符串,还接受其他简单的数据类型:

var e = new XElement ("date", DateTime.Now);e.SetValue (DateTime.Now.AddDays(1));Console.Write (e.Value);              // 2019-10-02T16:39:10.734375+09:00

我们可以只设置元素的 Value 属性,但这意味着手动将 DateTime 转换为字符串。这比调用 ToString 更复杂 — 它需要使用 XmlConvert 来获得符合 XML 的结果。

当你将传递到 XElement 或 XAttribute 的构造函数中时,非字符串类型也会发生同样的自动转换。这可确保日期时间的格式正确;true 以小写形式书写,双字母书写。负无穷大写为“-INF”。

获取值

若要反过来将值分析回基类型,只需将 XElement 或 XAttribute 强制转换为所需的类型。听起来它不应该起作用 - 但它确实有效!例如:

XElement e = new XElement ("now", DateTime.Now);DateTime dt = (DateTime) e;XAttribute a = new XAttribute ("resolution", 1.234);double res = (double) a;

元素或属性不会以本机方式存储 DateTime 或数字,它们始终存储为文本,然后根据需要进行分析。它也不会“记住”原始类型,因此您必须正确强制转换它以防止运行时错误。为了使您的代码健壮,您可以将强制转换放在 try / catch 块中,捕获 FormatException 。

XElement 和 XAttribute 上的显式强制转换可以解析为以下类型:

所有标准数值类型字符串、布尔值、日期时间、日期时间偏移量、时间跨度和指导可为空<>上述值类型的版本

强制转换为可为 null 的类型与 Element 和 Attribute 方法结合使用非常有用,因为如果请求的名称不存在,强制转换仍然有效。例如,如果 x 没有超时元素,则第一行会生成运行时错误,而第二行不会:

int timeout = (int) x.Element ("timeout");      // Errorint? timeout = (int?) x.Element ("timeout");    // OK; timeout is null.

您可以使用 ??算子。如果分辨率属性不存在,则计算结果为 1.0:

double resolution = (double?) x.Attribute ("resolution") ?? 1.0;

但是,如果元素或属性并且具有空(或格式不正确)的值,则强制转换为可为 null 的类型不会让您摆脱麻烦。为此,您必须捕获一个 格式异常 .

还可以在 LINQ 查询中使用强制转换。以下返回“约翰”:

var data = XElement.Parse (  @"<data>      <customer id='1' name='Mary' credit='100' />      <customer id='2' name='John' credit='150' />      <customer id='3' name='Anne' />    </data>");IEnumerable<string> query = from cust in data.Elements()                            where (int?) cust.Attribute ("credit") > 100                            select cust.Attribute ("name").Value;

强制转换为可为 null 的 int 可防止 Anne 出现 NullReferenceException,而 Anne 没有信用属性。另一种解决方案是在 where 子句中添加谓词:

where cust.Attributes ("credit").Any() && (int) cust.Attribute...

同样的原则也适用于查询元素值。

值和混合内容节点

给定 Value 的值,您可能想知道何时需要直接处理 XText 节点。答案是当你有混合内容时。例如:

<summary>An XAttribute is <bold>not</bold> an XNode</summary>

一个简单的 Value 属性不足以捕获摘要的内容。摘要元素包含三个子节点:一个 XText 节点,后跟一个 XElement,后跟另一个 XText 节点。以下是构造它的方法:

XElement summary = new XElement ("summary",                      new XText ("An XAttribute is "),                      new XElement ("bold", "not"),                      new XText (" an XNode")                    );

有趣的是,我们仍然可以查询摘要的值,而不会出现异常。相反,我们得到了每个孩子的值的串联:

An XAttribute is not an XNode

重新分配摘要的值也是合法的,代价是用一个新的XText节点替换所有以前的子节点。

自动 XText 级联

当您将简单内容添加到 XElement 时,X-DOM 会附加到现有的 XText 子项,而不是创建一个新的子项。在以下示例中,e1 和 e2 最终只有一个值为 HelloWorld 的子 XText 元素:

var e1 = new XElement ("test", "Hello"); e1.Add ("World");var e2 = new XElement ("test", "Hello", "World");

但是,如果您专门创建 XText 节点,则最终会得到多个子节点:

var e = new XElement ("test", new XText ("Hello"), new XText ("World"));Console.WriteLine (e.Value);             // HelloWorldConsole.WriteLine (e.Nodes().Count());   // 2

XElement 不会连接两个 XText 节点,因此保留节点的对象标识。

文件和声明XDocument

正如我们之前所说,XDocument 包装了一个根 XElement,并允许您添加 XDeclaration 、处理指令、文档类型和根级注释。XDocument 是可选的,可以忽略或省略:与 W3C DOM 不同,它不能作为粘合剂将所有内容保持在一起。

XDocument 提供与 XElement 相同的功能构造函数。因为它是基于XContainer的,所以它也支持、、和方法。然而,与XElement不同的是,XDocument只能接受有限的内容:AddXXXRemoveXXXReplaceXXX

单个 XElement 对象(“根”)单个 XDeclaration 对象单个 XDocumentType 对象(用于引用文档类型定义 [DTD])任意数量的 XProcessingInstruction 对象任意数量的 XComment 对象注意

其中,只有根 XElement 是强制性的,以便拥有有效的 XDocument 。XDeclaration 是可选的 - 如果省略,则在序列化期间应用默认设置。

最简单的有效 XDocument 只有一个根元素:

var doc = new XDocument (            new XElement ("test", "data")          );

请注意,我们没有包含 XDeclaration 对象。通过调用文档生成的文件。但是,保存仍将包含 XML 声明,因为默认情况下会生成一个 XML 声明。

下一个示例生成一个简单但正确的 XHTML 文件,说明 XDocument 可以接受的所有构造:

var styleInstruction = new XProcessingInstruction (  "xml-stylesheet", "href='styles.css' type='text/css'");var docType = new XDocumentType ("html",  "-//W3C//DTD XHTML 1.0 Strict//EN",  ";, null);XNamespace ns = ";;var root =  new XElement (ns + "html",    new XElement (ns + "head",      new XElement (ns + "title", "An XHTML page")),    new XElement (ns + "body",      new XElement (ns + "p", "This is the content"))  );var doc =  new XDocument (    new XDeclaration ("1.0", "utf-8", "no"),    new XComment ("Reference a stylesheet"),    styleInstruction,    docType,    root);doc.Save ("test.html");

结果如下:

<?xml version="1.0" encoding="utf-8" standalone="no"?><!--Reference a stylesheet--><?xml-stylesheet href='styles.css' type='text/css'?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"                      ";><html xmlns=";>  <head>    <title>An XHTML page</title>  </head>  <body>    <p>This is the content</p>  </body></html>

XDocument 有一个 Root 属性,用作访问文档的单个 XElement 的快捷方式。反向链接由 XObject 的 Document 属性提供,该属性适用于树中的所有对象:

Console.WriteLine (doc.Root.Name.LocalName);          // htmlXElement bodyNode = doc.Root.Element (ns + "body");Console.WriteLine (bodyNode.Document == doc);         // True

回想一下,文档的子级没有父级:

Console.WriteLine (doc.Root.Parent == null);          // Trueforeach (XNode node in doc.Nodes())  Console.Write (node.Parent == null);                // TrueTrueTrueTrue
注意

XDeclaration 不是 XNode,不会显示在文档的节点集合中,这与注释、处理指令和根元素不同。相反,它被分配给一个名为 声明 的专用属性。这就是为什么在最后一个示例中“True”重复四次而不是五次的原因。

XML 声明

标准 XML 文件以如下声明开头:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>

XML 声明可确保读取器正确解析和理解文件。XElement 和 XDocument 在发出 XML 声明时遵循以下规则:

使用文件名调用 Save 始终写入声明。使用 XmlWriter 调用 Save 将写入声明,除非 XmlWriter 另有指示。ToString 方法从不发出 XML 声明。注意

可以通过在构造 XmlWriter 时设置 XmlWriterSettings 对象的 OmitXmlStatement 和 ConformanceLevel 属性来指示 XmlWriter 不生成声明。我们将在第中对此进行描述。

是否存在 XDeclaration 对象对是否写入 XML 声明没有影响。XDeclaration 的目的是以两种方式:

使用什么文本编码在 XML 声明的编码和独立属性中放入什么(是否应编写声明)

XDeclaration 的构造函数接受三个参数,分别对应于属性版本、编码和独立。在以下示例中, 以 UTF-16 编码:

var doc = new XDocument (            new XDeclaration ("1.0", "utf-16", "yes"),            new XElement ("test", "data")          );doc.Save ("test.xml");
注意

XML 编写器会忽略您为 XML 版本指定的任何内容:它始终写入“1.0”。

编码必须使用IETF代码,如“utf-16”,就像它出现在XML声明中一样。

将声明写入字符串

假设我们要将XDocument序列化为一个字符串,包括XML声明。因为 ToString 不编写声明,所以我们需要使用 XmlWriter ,而不是:

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);Console.WriteLine (output.ToString());

结果如下:

<?xml version="1.0" encoding="utf-16" standalone="yes"?><test>data</test>

请注意,输出中有 UTF-16,即使我们在 XDeclaration 中明确请求了 UTF-8!这可能看起来像一个错误,但实际上,XmlWriter非常聪明。由于我们写入的是字符串而不是文件或流,因此无法应用 UTF-16(字符串内部存储的格式)以外的任何编码。因此,XmlWriter写“utf-16”以免撒谎。

这也解释了为什么 ToString 方法不发出 XML 声明。想象一下,您没有调用 Save ,而是执行以下操作将 XDocument 写入文件:

File.WriteAllText ("data.xml", doc.ToString());

就目前而言,将缺少XML声明,使其不完整但仍然可以解析(您可以推断文本编码)。但是如果 ToString() 发出一个 XML 声明,data 实际上会包含一个的 ( encoding=“utf-16”),这可能会阻止它被读取,因为 WriteAllText 使用 UTF-8 编码。

名称和命名空间

正如 .NET 类型可以具有命名空间一样,XML 元素和属性也可以具有命名空间。

XML 命名空间实现了两件事。首先,与 C# 中的命名空间非常相似,它们有助于防止命名冲突。当您将数据从一个 XML 文件合并到另一个 XML 文件时,这可能会成为一个问题。其次,命名空间为名称赋予含义。例如,“nil”这个名字可能意味着任何东西。但是,在 命名空间中,“nil”表示等效于 C# 中的 null,并附带有关如何应用它的特定规则。

由于 XML 命名空间是造成混淆的重要来源,因此我们首先介绍一般的命名空间,然后继续讨论如何在 LINQ to XML 中使用它们。

XML 中的命名空间

假设我们想在命名空间 OReilly.Nutshell.CSharp 中定义一个客户元素。有两种方法可以继续。第一种是使用 xmlns 属性:

<customer xmlns="OReilly.Nutshell.CSharp"/>

XMLNS 是一个特殊的保留属性。以这种方式使用时,它执行两个功能:

它为相关元素指定命名空间。它为所有后代元素指定默认命名空间。

这意味着在下面的示例中,地址和邮政编码隐式驻留在 OReilly.Nutshell.CSharp 命名空间中:

<customer xmlns="OReilly.Nutshell.CSharp">  <address>    <postcode>02138</postcode>  </address></customer>

如果我们希望地址和邮政编码命名空间,我们需要这样做:

<customer xmlns="OReilly.Nutshell.CSharp">  <address xmlns="">    <postcode>02138</postcode>     <!-- postcode now inherits empty ns -->  </address></customer>
前缀

指定命名空间的另一种方法是使用。前缀是分配给命名空间以保存键入的别名。使用前缀有两个步骤:前缀并。您可以同时执行这两项操作:

<nut:customer xmlns:nut="OReilly.Nutshell.CSharp"/>

这里正在发生两件截然不同的事情。在右边,xmlns:nut=“...”定义一个名为 nut 的前缀,并使其可用于此元素及其所有后代。在左侧,nut:customer 将新分配的前缀分配给 customer 元素。

带前缀的元素为后代定义默认命名空间。在以下 XML 中,名字具有空命名空间:

<nut:customer xmlns:nut="OReilly.Nutshell.CSharp">  <firstname>Joe</firstname></customer>

要给名字命名OReilly.Nutshell.CSharp前缀,您必须执行以下操作:

<nut:customer xmlns:nut="OReilly.Nutshell.CSharp">  <nut:firstname>Joe</firstname></customer>

为了方便后代,您还可以定义一个或多个前缀,而无需将任何前缀分配给父元素本身。下面定义了两个前缀 i 和 z ,同时将客户元素本身保留为空命名空间:

<customer xmlns:i=";          xmlns:z=";>  ...</customer>

如果这是根节点,则整个文档将具有触手可及的 i 和 z。当元素需要从多个命名空间绘制时,前缀很方便。

请注意,此示例中的两个命名空间都是 URI。使用(您拥有的)URI 是标准做法:它确保命名空间的唯一性。因此,在现实生活中,我们的客户元素更有可能是

<customer xmlns=";/>

艺术

<nut:customer xmlns:nut=";/>
属性

您也可以将命名空间分配给属性。主要区别在于属性始终需要前缀。例如:

<customer xmlns:nut="OReilly.Nutshell.CSharp" nut:id="123" />

另一个区别是非限定属性始终具有空命名空间:它从不从父元素继承默认命名空间。

属性往往不需要命名空间,因为它们的含义通常是元素的本地含义。通用或元数据属性(如 W3C 定义的 nil 属性)例外:

<customer xmlns:xsi=";>  <firstname>Joe</firstname>  <lastname xsi:nil="true"/></customer>

这明确表示姓氏为 nil(在 C# 中为 null)而不是空字符串。因为我们使用了标准命名空间,所以通用分析实用程序可以肯定地知道我们的意图。

在 X-DOM 中指定命名空间

到目前为止,在本章中,我们只对 XElement 和 XAttribute 名称使用了简单的字符串。简单字符串对应于具有空命名空间的 XML 名称,更类似于全局命名空间中定义的 .NET 类型。

有几种方法可以指定 XML 命名空间。第一种是将其括在大括号中,在本地名称之前:

var e = new XElement ("{}customer", "Bloggs");Console.WriteLine (e.ToString());

这将生成生成的 XML:

<customer xmlns=";>Bloggs</customer>

第二种(也是性能更高的)方法是使用 XNamespace 和 XName 类型。以下是它们的定义:

public sealed class XNamespace{  public string NamespaceName { get; }}public sealed class XName     // A local name with optional namespace{  public string LocalName { get; }  public XNamespace Namespace { get; }   // Optional}

这两种类型都定义了字符串的隐式强制转换,因此以下内容是合法的:

XNamespace ns   = ";;XName localName = "customer";XName fullName  = "{}customer";

XNamespace 还重载了 + 运算符,允许您在不使用大括号的情况下将命名空间和名称组合到 XName 中:

XNamespace ns = ";;XName fullName = ns + "customer";Console.WriteLine (fullName);     // {}customer

X-DOM 中所有接受元素或属性名称的构造函数和方法实际上都接受 XName 对象而不是字符串。您可以替换字符串的原因(就像我们迄今为止的所有示例一样)是因为隐式强制转换。

无论是为元素还是属性指定命名空间都是相同的:

XNamespace ns = ";;var data = new XElement (ns + "data",              new XAttribute (ns + "id", 123)           );
X-DOM 和默认命名空间

X-DOM 忽略默认命名空间的概念,直到实际输出 XML 为止。这意味着当你构造一个子 XElement 时,如果需要,你必须显式地给它一个命名空间;它从父级继承:

XNamespace ns = ";;var data = new XElement (ns + "data",             new XElement (ns + "customer", "Bloggs"),             new XElement (ns + "purchase", "Bicycle")           );

但是,X-DOM 在读取和输出 XML 时确实应用默认命名空间:

Console.WriteLine (data.ToString());OUTPUT:  <data xmlns=";>    <customer>Bloggs</customer>    <purchase>Bicycle</purchase>  </data>Console.WriteLine (data.Element (ns + "customer").ToString());OUTPUT:  <customer xmlns=";>Bloggs</customer>

如果在未指定命名空间的情况下构造 XElement 子项,换句话说

XNamespace ns = ";;var data = new XElement (ns + "data",             new XElement ("customer", "Bloggs"),             new XElement ("purchase", "Bicycle")           );Console.WriteLine (data.ToString());

相反,你会得到这个结果:

<data xmlns=";>  <customer xmlns="">Bloggs</customer>  <purchase xmlns="">Bicycle</purchase></data>

另一个陷阱是在导航 X-DOM 时无法包含命名空间:

XNamespace ns = ";;var data = new XElement (ns + "data",             new XElement (ns + "customer", "Bloggs"),             new XElement (ns + "purchase", "Bicycle")           );XElement x = data.Element (ns + "customer");    // okXElement y = data.Element ("customer");         // null

如果在未指定命名空间的情况下构建 X-DOM 树,则随后可以将每个元素分配给单个命名空间,如下所示:

foreach (XElement e in data.DescendantsAndSelf())  if (e.Name.Namespace == "")    e.Name = ns + e.Name.LocalName;
前缀

X-DOM 对待前缀就像对待命名空间一样:纯粹是一个序列化函数。这意味着您可以选择完全忽略前缀问题 - 然后过得去!您可能希望这样做的唯一原因是为了在输出到 XML 文件时提高效率。例如,考虑一下:

XNamespace ns1 = ";;XNamespace ns2 = ";;var mix = new XElement (ns1 + "data",            new XElement (ns2 + "element", "value"),            new XElement (ns2 + "element", "value"),            new XElement (ns2 + "element", "value")          );

默认情况下,XElement 将按如下方式对此进行序列化:

<data xmlns=";>  <element xmlns=";>value</element>  <element xmlns=";>value</element>  <element xmlns=";>value</element></data>

如您所见,存在一些不必要的重复。解决方案更改构造 X-DOM 的方式,而是在编写 XML 之前提示序列化程序。为此,请添加属性,定义要应用的前缀。这通常在根元素上完成:

mix.SetAttributeValue (XNamespace.Xmlns + "ns1", ns1);mix.SetAttributeValue (XNamespace.Xmlns + "ns2", ns2);

这会将前缀“ns1”分配给我们的XNamespace变量ns1,并将“ns2”分配给ns2。X-DOM 在序列化时自动选取这些属性,并使用它们来压缩生成的 XML。这是现在在mix上调用ToString的结果:

<ns1:data xmlns:ns1=";          xmlns:ns2=";>  <ns2:element>value</ns2:element>  <ns2:element>value</ns2:element>  <ns2:element>value</ns2:element></ns1:data>

前缀不会更改构造、查询或更新 X-DOM 的方式 — 对于这些活动,您将忽略前缀的存在并继续使用全名。前缀仅在与 XML 文件或流相互转换时发挥作用。

序列化属性时也遵循前缀。在以下示例中,我们使用 W3C 标准属性将客户的出生日期和信用记录为“nil”。突出显示的行可确保序列化前缀,而不会重复不必要的命名空间:

XNamespace xsi = ";;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)             )           );

这是它的XML:

<customers xmlns:xsi=";>  <customer>    <lastname>Bloggs</lastname>    <dob xsi:nil="true" />    <credit xsi:nil="true" />  </customer></customers>

为了简洁起见,我们预先声明了 nil XAttribute ,以便我们可以在构建 DOM 时使用它两次。您可以引用同一属性两次,因为它会根据需要自动复制。

附注

您可以使用批注将自定义数据附加到任何 XObject。注释仅供您私人使用,并被 X-DOM 视为黑盒。如果您曾经在 Windows 窗体或 Windows Presentation Foundation (WPF) 控件上使用过 Tag 属性,那么您将熟悉这个概念 - 区别在于您有多个批注,并且您的批注可以您可以创建其他类型的注释,更不用说覆盖了。

XObject 上的以下方法添加和删除批注:

public void AddAnnotation (object annotation)public void RemoveAnnotations<T>()     where T : class

以下方法检索批注:

public T Annotation<T>()               where T : classpublic IEnumerable<T> Annotations<T>() where T : class

每个批注都按其类型进行键控,该类型必须是引用。以下内容添加然后检索字符串批注:

XElement e = new XElement ("test");e.AddAnnotation ("Hello");Console.WriteLine (e.Annotation<string>());   // Hello

可以添加同一类型的多个批注,然后使用 批注 方法检索匹配。

但是,公共类型(如字符串)不能成为很好的键,因为其他类型的代码可能会干扰您的批注。更好的方法是使用内部或(嵌套)私有类:

class X{  class CustomData { internal string Message; }   // Private nested type  static void Test()  {    XElement e = new XElement ("test");    e.AddAnnotation (new CustomData { Message = "Hello" } );    Console.Write (e.Annotations<CustomData>().First().Message);  // Hello  }}

若要删除批注,还必须有权访问密钥的类型:

e.RemoveAnnotations<CustomData>();
投影到 X-DOM 中

到目前为止,我们已经演示了如何使用 LINQ 从 X-DOM 获取数据。还可以使用 LINQ 查询投影到 X-DOM 。源可以是 LINQ 可以查询的任何内容,例如:

EF 核心实体类本地集合另一个 X-DOM

无论来源如何,使用 LINQ 发出 X-DOM 的策略都是相同的:首先编写生成所需 X-DOM 形状的表达式,然后围绕该表达式生成 LINQ 查询。

例如,假设我们要将客户从数据库检索到以下 XML 中:

<customers>  <customer id="1">    <name>Sue</name>    <buys>3</buys>  </customer>  ...</customers>

我们首先使用简单的文字为 X-DOM 编写函数构造表达式:

var customers =  new XElement ("customers",    new XElement ("customer", new XAttribute ("id", 1),      new XElement ("name", "Sue"),      new XElement ("buys", 3)    )  );

然后,我们将其转换为投影,并围绕它生成一个 LINQ 查询:

var customers =  new XElement ("customers",    // We must call AsEnumerable() due to a bug in EF Core.    from c in dbContext.Customers.AsEnumerable()    select      new XElement ("customer", new XAttribute ("id", c.ID),        new XElement ("name", c.Name),        new XElement ("buys", c.Purchases.Count)      )    );
注意

由于 EF Core 中的 bug 需要调用 AsEnumerable(计划在以后的版本中进行修复)。修复错误后,删除对 AsEnumerable 的调用将防止每次调用 c.Purchases.Count 时出现往返,从而提高效率。

结果如下:

<customers>  <customer id="1">    <name>Tom</name>    <buys>3</buys>  </customer>  <customer id="2">    <name>Harry</name>    <buys>2</buys>  </customer>    ...</customers>

通过分两步构造相同的查询,我们可以更清楚地看到它是如何工作的。第一:

IEnumerable<XElement> sqlQuery =  from c in dbContext.Customers.AsEnumerable()  select    new XElement ("customer", new XAttribute ("id", c.ID),      new XElement ("name", c.Name),      new XElement ("buys", c.Purchases.Count)    );

此内部部分是投影到 XElement s 中的普通 LINQ 查询。这是第二步:

var customers = new XElement ("customers", sqlQuery);

这将构造根 XElement 。唯一不寻常的是,内容sqlQuery不是单个XElement,而是IQueryable<XElement>,它实现了IEnumerable<XElement>。请记住,在处理 XML 内容时,会自动枚举集合。因此,每个 XElement 都作为子节点添加。

消除空元素

假设在前面的示例中,我们还希望包含客户最近高价值购买的详细信息。我们可以按如下方式执行此操作:

var customers =  new XElement ("customers",    // The AsEnumerable call can be removed when the EF Core bug is fixed.    from c in dbContext.Customers.AsEnumerable()    let lastBigBuy = (from p in c.Purchases                      where p.Price > 1000                      orderby p.Date descending                      select p).FirstOrDefault()    select      new XElement ("customer", new XAttribute ("id", c.ID),        new XElement ("name", c.Name),        new XElement ("buys", c.Purchases.Count),        new XElement ("lastBigBuy",          new XElement ("description", lastBigBuy?.Description),          new XElement ("price", lastBigBuy?.Price ?? 0m)        )      )  );

但是,对于没有高价值购买的客户,这会发出空元素。(如果它是本地查询而不是数据库查询,则会抛出 NullReferenceException。在这种情况下,最好完全省略最后一个BigBuy节点。我们可以通过将 lastBigBuy 元素的构造函数包装在条件运算符中来实现这一点:

    select      new XElement ("customer", new XAttribute ("id", c.ID),        new XElement ("name", c.Name),        new XElement ("buys", c.Purchases.Count),        lastBigBuy == null ? null :          new XElement ("lastBigBuy",            new XElement ("description", lastBigBuy.Description),            new XElement ("price", lastBigBuy.Price)

对于没有 lastBigBuy 的客户,将发出一个空值而不是一个空的 XElement。这就是我们想要的,因为空内容会被忽略。

流式传输投影

如果您投影到 X-DOM 中只是为了保存它(或在其上调用 ToString),则可以通过 XStreamingElement 提高内存效率。XStreamingElement 是 XElement 的精简版本,它将语义应用于其子内容。要使用它,您只需将外部的 XElement s 替换为 XStreamingElement s:

var customers =  new XStreamingElement ("customers",    from c in dbContext.Customers    select      new XStreamingElement ("customer", new XAttribute ("id", c.ID),        new XElement ("name", c.Name),        new XElement ("buys", c.Purchases.Count)      )    );customers.Save ("data.xml");

传递到 XStreamingElement 的构造函数中的查询不会枚举,直到你对元素调用 Save 、ToString 或 WriteTo;这可以防止一次将整个 X-DOM 加载到内存中。另一方面是查询被重新评估,如果您重新保存 .此外,您不能遍历 XStreamingElement 的子内容 — 它不会公开元素或属性等方法。

XStreamingElement 不是基于 XObject 或任何其他类的,因为它具有如此有限的一组成员。除了 保存 、 ToString 和 WriteTo 之外,它拥有的唯一成员如下:

Add 方法,它接受构造函数等内容“名称”属性

XStreamingElement 不允许您以流式传输方式内容 — 为此,您必须将 XmlReader 与 X-DOM 结合使用。我们将在第 章的中描述如何执行此操作。

标签: #htmlobject层级