65.9K
CodeProject 正在变化。 阅读更多。
Home

System.Xml 4.0 中的常见陷阱

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (9投票s)

2010 年 4 月 20 日

Ms-PL

24分钟阅读

viewsIcon

45316

downloadIcon

196

使用 DTD、重复命名空间、有意义的空白和 SchemaSet 的陷阱;包括 System.XML 4.0 中的新功能和已过时功能。

引言

这个简短的会话将展示我自己在 System.Xml 及其子命名空间中发现的一些陷阱的研究成果。我们将讨论

  • XDocumentXmlReader/Writer 之间不明显的差异
  • 重复命名空间声明的命名空间处理
  • 如何正确处理混合内容 XML 文件
  • StreamSchemaSet 的难题

最后,我们将了解 .NET 4.0 中 System.Xml 不同命名空间中哪些功能已过时,哪些功能是全新的。

您将需要:

  • Visual Studio 2010 (大部分功能也适用于 VS2008 SP1)
  • 扎实的 C# 和 XML 基础,了解 XPath 和 XSLT 以理解所有示例。如果您还了解 DTD 和 XML Schema,那就更好了!

本会话无意教您如何使用 C# 处理基本 XML 文件,您应该已经了解这些,抱歉!

如果您很着急,请跳到每个主题的结论部分,中间的所有内容都是背景信息或证明。

我希望这项研究能帮助您将来避免错误,并帮助您注意 System.Xml 中发现的一些特定陷阱。

基础知识

处理原生 XML 有四种主要方式

  • .NET 类的序列化(未讨论)
  • 使用 XmlReaderXmlWriter
  • 使用 XmlDocument
  • 使用 XDocument

您应该知道如何使用所有这些。

作为一个快速回顾,这里是如何创建 XDocument 的示例

XDocument document = new XDocument("root",
    new XElement("first-child",
        new XAttribute("an-attribute", "with a value"),
        "some text inside"));

如您所见,XDocument 是处理大多数情况时要小心使用的类。

陷阱

读写默认设置检查

理想情况下,每个重载的默认设置都应该相同。.NET 库在 XML 读写方面使用 XmlReader.CreateXmlWriter.Create 静态方法时表现出可预测性。也就是说,如果您考虑到一些注意事项。然而,有些事情您可能没有预料到。请继续阅读。

请参阅随附项目:XmlReaderWriterDefaults

XmlWriterSettings

如果您知道要留意什么,工厂方法 XmlWriter.Create 在构造函数调用中具有相当一致的行为。

CloseOutputEncoding 是不同构造函数之间的主要区别。

基于内存结构(如 StringBuilder)的构造函数默认使用 Microsoft 称之为“Unicode”的编码(这不适用于 Stream,它们的内存表示本身不是“文本”)。其他基于不使用 StringBuilder 的文件流的构造函数默认使用 UTF-8(显然也是 Unicode,但与“Unicode”或 UTF-16 不同)。对于使用文件名的重载,CloseOutputtrue。这意味着您需要自行关闭流,例如,通过使用“using”关键字,除非您提供文件名;在这种情况下,当读取器或写入器被释放时,它会自动关闭。

除了“NamespaceHandling”,.NET 2.0 和 4.0 之间没有显著变化,代码应该保持兼容。

以下是测试过的每个构造函数调用共享的默认值

CheckCharacters

TRUE

ConformanceLevel

文档

Indent

FALSE

IndentChars

(空格)

NamespaceHandling(在 .NET 4.0 中添加)

默认值

NewLineChars

(换行)

NewLineHandling

替换

NewLineOnAttributes

FALSE

OmitXmlDeclaration

FALSE

每个构造函数调用之间的差异是

构造函数

CloseOutput

编码

XmlWriter 2.0

XmlWriter.Create(dummyStream)

FALSE

UTF8Encoding

XmlWriter.Create("c:\in.xml")

TRUE

UTF8Encoding

XmlWriter.Create(dummyBuilder)

FALSE

UnicodeEncoding

XmlWriter.Create(new StringWriter(dummyBuilder)

FALSE

UnicodeEncoding

XmlWriter.Create(Create("c:\in2.xml"))

TRUE

UTF8Encoding

XmlWriter.Create(dummyStream, defaultSettings)

FALSE

UTF8Encoding

XmlWriter.Create(@"c:\in.xml", defaultSettings)

TRUE

UTF8Encoding

XmlWriter.Create(dummyBuilder, defaultSettings)

FALSE

UnicodeEncoding

XmlWriter.Create(new StringWriter(dummyBuilder), defaultSettings)

FALSE

UnicodeEncoding

XmlWriter.Create(XmlWriter.Create(@"c:\in2.xml", defaultSettings))

TRUE

UTF8Encoding

XmlWriter 4.0

XmlWriter.Create(dummyStream)

FALSE

UTF8Encoding

XmlWriter.Create("c:\in.xml")

TRUE

UTF8Encoding

XmlWriter.Create(dummyBuilder)

FALSE

UnicodeEncoding

XmlWriter.Create(new StringWriter(dummyBuilder)

FALSE

UnicodeEncoding

XmlWriter.Create(dummyStream, defaultSettings)

FALSE

UTF8Encoding

XmlWriter.Create(@"c:\in.xml", defaultSettings)

TRUE

UTF8Encoding

XmlWriter.Create(dummyBuilder, defaultSettings)

FALSE

UnicodeEncoding

XmlWriter.Create(new StringWriter(dummyBuilder), defaultSettings)

FALSE

UnicodeEncoding

XmlWriter.Create(XmlWriter.Create(@"c:\in2.xml", defaultSettings))

TRUE

UTF8Encoding

XmlReaderSettings

工厂方法 XmlReader.Create 在所有测试的构造函数调用中都具有一致的行为,无论是在 .NET 2.0 还是 .NET 4.0 中。ProhibitDtd 现在已过时,并由 DtdProcessing 替代。这意味着所有旧代码都会有警告,但这些警告很容易修复。EncodingCloseInput 设置的行为与写入器设置类似:如果您将文件名传递给工厂方法,流将自动关闭。如果您使用 StringBuilder 作为写入的基础,编码是“Unicode”(UTF-16)。

CheckCharacters

TRUE

CloseInput

FALSE

ConformanceLevel

文档

DtdProcessing (在 .NET 4.0 中添加)

禁止(与 ProhibitDtd=true 兼容)

IgnoreComments

FALSE

IgnoreProcessingInstructions

FALSE

IgnoreWhitespace

FALSE

LineNumberOffset

0

LinePositionOffset

0

MaxCharactersFromEntities

0

MaxCharactersInDocument

0

NameTable

(空)

ProhibitDtd

TRUE

模式

System.Xml.Schema.XmlSchemaSet

ValidationFlags

ProcessIdentityConstraints, AllowXmlAttributes

ValidationType

如何处理混合内容的空白

请参阅随附项目:XsltWhiteSpaceXdocumentWhitespace

关于有意义和无意义的空白,以及美化打印

空白是制表符、换行符或空格(但不是“不间断空格”——在 HTML 中是字符实体  ——它是一种特殊的空格,可防止换行)。

美化打印 XML 文档意味着对其进行缩进,使其更易于人类阅读。

<div>
   <p>
         <em>C# :</em>
         <span class="description">My fave</span>
   </p>
   <br/>
   <p>
         <em>VB.NET :</em>
         <span class="description">Jean's fave</span>
   </p>
</div>

这个 XML 是美化打印的。

原始文件可能像这样

<div>      
<p><em>C# :</em> <span class="description">My fave</span></p><br/>
<p><em>VB.NET :</em> <span class="description">Jean's fave</span></p>
</div>

如果 XML 中未指定 DTD(文档类型声明),则空白是不可忽略的[1]。DTD 可以指定哪些空白可以忽略,哪些不能忽略。XML 解析器不知道空白是否重要(有意义),并且应该保留所有未被 DTD 指示丢弃的空白。XML 规范不强制这样做,但这是唯一“安全”的默认设置,IBM 和 Oracle 等公司也发现了这一点。默认行为应该是保留空白(但您稍后会看到,这不是 XDocumentXmlDocument 的实现方式)。请注意上述文档中插入了制表符。制表符是有效的 XML 空白。内存中的 DOM 不会改变,直到文档从包含新制表符的输出中再次解析——例如在对其执行 XSL 转换之后。

类似地,XSLT 处理器不知道如何处理空白,其默认行为是保留空格。(参见:https://w3schools.org.cn/XSL/el_preserve-space.asp[2]。)

要使空格不重要,您需要在样式表的顶部使用 <xsl:strip-space> 元素。为了验证,我们将对两个版本运行以下样式表

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> 
    <xsl:output method="text"
/>
</xsl:stylesheet>

这是一个“空”样式表。为元素提供 <xsl:template> 会覆盖默认处理,但我们不会这样做。任何元素的默认处理是剥离元素本身,剥离属性,并保留所有文本,这正是我们想要做的。

  • 未美化打印版本
  • C# : My fave
    VB.NET : Jean's fave
  • 美化打印版本
  • C# :
    My fave
    
    
    
    VB.NET :
    Jean's fave

如您所见,美化打印实际上改变了文档内容!在大多数情况下,这无害,特别是对于序列化的 .NET 或数据对象,但对于 XHTML 等标记语言以及其他混合内容格式,这是一种不便。要真正获得您需要的结果,XSLT 将变得更复杂,有时会使用变量和 normalize-space 函数。有关很好的示例,请参阅 MSDN:http://msdn.microsoft.com/en-us/library/ms256063.aspx

XDocument、XmlDocument 和 XmlReaders/Writers 在空白处理方面的不一致

鉴于此,XmlReader/WriterXDocument 之间在处理空白方面存在一个令人不快的差异。(参见http://msdn.microsoft.com/en-us/library/bb387014.aspx。)

XDocumentXmlReader 之间的默认值不同

XDocument

XmlReader

LoadOptions.PreserveWhitespace

XmlReaderSettings.IgnoreWhitespace = false(默认)

(未指定:不保留空白,默认

XmlReaderSettings.IgnoreWhitespace = true

这意味着使用 XmlReader 意味着您将使用带有默认设置的“标准”XML 处理。使用 XDocument 意味着在默认设置下,混合内容的间距会出现怪异现象……相当出乎意料,对吧?

此外,XmlReaderSettingsLoadOptions 可能会冲突。当加载 XDocument 时传递“LoadOptions”参数,LoadOption 将按预期使用,除非使用 XmlReader/Writer;在这种情况下,将使用读取器/写入器的选项

XDocument.Load(string)

忽略

XDocument.Load(string,LoadOptions.PreserveWhitespace)

已保留

XDocument.Load(XmlReader, LoadOptions.PreserveWhitespace), XmlReaderSettings.IgnoreWhitespace=false

已保留

XDocument.Load(XmlReader, LoadOptions.None), XmlReaderSettings.IgnoreWhitespace=true

忽略

XDocument.Load(XmlReader, LoadOptions.PreserveWhitespace, LoadOptions.None), XmlReaderSettings.IgnoreWhitespace=false

已保留

XDocument.Load(XmlReader), XmlReaderSettings.IgnoreWhitespace=true

忽略

XmlReader, XmlReaderSettings.IgnoreWhitespace=false

已保留

XmlReader, XmlReaderSettings.IgnoreWhitespace=true

忽略

XDocument 出现以来,这种困难就一直存在,测试表明它在 .NET 4.0 中仍然存在——API 在这方面没有改变。

为什么这很重要?看看结果。我们有一段包含两个元素的文本:<em><i>。在原始文档中,两者之间有一个空格。这就是 XML 所说的“混合内容”。请注意,在某些忽略空白的重载中,空格是如何被剥离的。空白是有意义的,XSLT 处理器通常会保留这个空格。结果是,XDocument 的“Value”属性(本质上与未定义任何模板的 XSLT 相同)存在差异,具体取决于您的设置

  • 空白未保留
  • Paragraph1Paragraph2
  • 空白已保留
  • Paragraph1 Paragraph2

同样,如果您正在序列化类,这并不是很重要,但如果您正在对文本进行转换,那么当您在不使用 LoadOptions 的情况下使用 XDocument 时,任何混合内容的文本在渲染时都会缺少空格,这很重要。

对于写入 XDocument 也有类似的情况

XDocument.Save 有一个 SaveOptions 参数,可以设置为禁用格式化。这与将 XmlWriterSettings 的“Indent”属性设置为“false”效果相同。默认情况下,保存时启用缩进,但这并不总是您想要的,并且可能导致您的 XSLT 需要大量的“normalize-space()”调用来处理“混合内容”。

XDocument

XmlWriter

SaveOptions.DisableFormatting

XmlWriterSettings.Indent = false默认

(未指定:使用“美化打印”XML,默认

XmlWriterSettings.Indent = true

然而,XmlWriterSettingsSaveOptions 不会冲突:在 XDocument.Save 上没有允许同时提供 XmlWriterSaveOption 的重载,这让您更容易操作

XDocument.Save(TextWriter, SaveOptions.DisableFormatting)

未缩进

XDocument.Save(TextWriter)

已缩进

XDocument.Save(XmlWriter), XmlWriterSettings.Indent=true

已缩进

XDocument.Save(XmlWriter), XmlWriterSettings.Indent=false

未缩进

对于 ToString(SaveOptions),同样的原则也适用:默认的 ToString() 会缩进您的 XML;如果您不希望这样做,您需要明确告知它。ToString() 不包含 XML 声明。

默认情况下,XDocument 应用美化打印,而 XmlWriter 不会。默认情况下,XDocument 在加载时再次剥离空格,但可能会剥离比保存时添加的更多的空格。XmlReader 默认情况下不剥离任何空格。

剩下的就是 XmlDocument 类了。这个类会如何表现呢?准备好感到困惑吧……

例如

using (XmlReader rdr =
XmlReader.Create("source.xml", new XmlReaderSettings
{
    IgnoreWhitespace = false
}))
{
    XmlDocument doc = new XmlDocument();
    doc.Load(rdr);
    return doc;
}

这段代码片段剥离了空白,即使您明确告诉它保留它们!具有以下内容的源文档(为适应一行而缩短)

..<div><em>Paragraph1</em> <span>Paragraph2</span></div>..

实际上被读取为以下内容

..<div><em>Paragraph1</em><span>Paragraph2</span></div>..

因此,即使将 XmlReaderSettings 设置为保留空白,XmlDocument 也会将其剥离。文档的读取受到了影响。测试了这些不同的方法,都得到了相同的结果

  • XmlDocument.LoadXmlReaderSettings IgnoreWhitespace=true
  • XmlDocument.ParseXml
  • XmlDocument.LoadXmlReaderSettings IgnoreWhitespace=false
  • XmlDocument.Load 不带 XmlReaderSettings

所有这些都产生了剥离空格的结果,无论 XmlReaderSettings 如何。

为什么会这样呢?因为,与 XDocumentXmlReader/Writer 设置优先不同,当涉及到空白时,XmlDocument 在加载之前使用“PreserveWhitespace”属性。此属性会覆盖文档读取的设置,并且默认情况下,它设置为 false

简而言之,要正确使用带有空白的 XmlDocument 类,您必须使用类似这样的代码

using (XmlReader rdr =
XmlReader.Create("source.xml"))
{
    XmlDocument doc = new XmlDocument
{ PreserveWhitespace = true }
    doc.Load(rdr);
    return doc;
}

另一个可能的解决方案是在您的源文件中添加 xml:space="preserve" 属性

<div xml:space="preserve"><em>Paragraph1</em><span>Paragraph2</span></div>

这样 XmlDocument *确实*保留了空白。但这非常令人烦恼。

您可以在 http://www.ibm.com/developerworks/xml/library/x-tipwhitesp.html 上阅读更多内容。

结论

那么解析包含混合内容的 XML 的最安全方法是什么?

  • 加载时,明确保留空白
    • XmlReader + XmlReaderSettings.IgnoreWhitespace = false(默认)
    • XDocument.Load(LoadOptions.PreserveWhitespace)
  • 保存中间结果时,不要缩进
    • XmlWriter + XmlWriterSettings.Indent = false(默认)
    • XDocument.Save(SaveOptions.DisableFormatting)
  • 或者,只使用 XDocument 使用 XmlReader/Writer 的重载,在这种情况下,将使用 XmlReader/Writer 设置。这是我个人偏爱的方法——总是使用我自己的读取器和写入器。
  • 使用 XmlDocument 时,XmlReader/WriterSettings 会被忽略——您需要将 PreserveWhitespace 属性设置为 true

这表明对于混合内容,您不能使用 XDocument 类中静态方法的最简单重载。这在 .NET 3.5 和 .NET 4.0 中保持完全相同,但会引起混淆。理论上,XDocument 的解释过于自由,假设空白可以安全忽略,以安全地处理标记文本文档,因为它需要每次仔细检查调用。实际上,大多数由此非标准行为引起的错误通常不会阻塞,除非您从事出版或标记行业,其中 XML 的解析难度更高,而不仅仅用作人类可读、可互换的数据存储格式。XDocument 的默认值针对数据,而不是标记文本。XmlDocument 也是如此。

如何处理古怪的命名空间

请参阅随附项目:Namespaces

命名空间很方便,但它们也会让您头疼不已。

在下面的 XML 文档示例中,local-disk 元素上存在一个重复的命名空间声明。我还包含了两次相同的命名空间 URI,但在同一元素上使用了不同的前缀。

<laptop
xmlns:work="http://hard.work.com"
xmlns:work2="http://too.hard.work.com"
xmlns:work3="http://hard.work.com">
  <work2:drive>
    <local-disk
xmlns:work="http://hard.work.com">C:\</local-disk>
   
<work:network-drive>Z:\</work:network-drive>
  </work2:drive>
</laptop>

使用删除重复命名空间的新选项加载此文件会产生以下结果

<laptop xmlns:work="http://hard.work.com"
   xmlns:work2="http://very.hard.work.com"
   xmlns:work3="http://hard.work.com">
     <work2:drive>
       <local-disk>C:\</local-disk>
         <work3:network-drive>Z:\</work3:network-drive>
     </work2:drive>
</laptop>

请注意,对“work”命名空间的引用已替换为“work3”命名空间,并且 local-disk 元素上的重复命名空间声明已删除。

此表解释了加载 XDocument 如何影响古怪的命名空间

祖先命名空间中存在重复的 URI,但别名不同

元素采用最后定义的命名空间别名,即使它们最初是用另一个别名编写的

元素命名空间中存在重复的 URI,且别名相同

无法加载文档

命名空间和别名也定义在祖先上,默认

重复的命名空间声明保持原样读取

命名空间和别名也定义在祖先上,并且使用了 NamespaceHandling.OmitDuplicates

重复的命名空间声明已从子元素中删除

处理 DTD

请参阅随附项目:UsingDTDs

使用 DtdProcessing 枚举,可以通过 XmlReaderSettingsXmlReader 设置,您可以允许使用 DTD。默认情况下,不允许使用 DTD,因为它是一个 URI,由文档本身提供,处理器被指示访问。这可能用于分布式攻击或其他安全风险——例如,有人将 DTD 放入您正在解析的 20,000 个文档中,您的服务器将每隔 0.05 秒尝试连接到攻击者选择的 URI。

然而,如果一个文档有 DTD,那么它可能无法解析(这意味着无法生成 DOM),因为文档可能包含在 DTD 中描述的实体。.NET 4.0 现在包含了加载包含 DTD 的 XML 文档的选项,而无需实际获取 DTD 并访问提及的 URI,使用 XML Reader。这在 .NET 3.5 中已经可以使用 XDocument 实现。

禁止

不允许使用 DTD,如果您尝试加载包含 DTD 的文档,则会发生异常(XmlReaderSettings 的默认值)。

解析

允许 DTD,如果找到,它们将通过读取器中的 XmlResolver 下载并作为文档的一部分进行解析。如果也启用了验证,则 DTD 中的错误、无法找到 DTD 或文档不遵守 DTD 将导致抛出异常。

Ignore

允许 DTD,但被忽略。DTD 未加载。如果 XML 文档包含在 DTD 中定义的实体(DTD 不仅仅是验证,它们还可以定义字符实体,例如),则对象无法成功加载并抛出异常(XDocument 的默认值,在 .NET 3.5 和 .NET 4.0 中都是如此)

需要注意的一点是,当使用带 XmlReader 的重载时,XDocument.Load 会使用 XmlReader 的 DTD 设置。

XDocument.Load(string)

忽略

XmlReader,没有 XmlReaderSettings

禁止

XmlReader

XmlReaderSettings.DtdProcessing=DtdProcessing.Parse

已解析,未验证

XmlReader

XmlReaderSettings.DtdProcessing=DtdProcessing.Parse

XmlReaderSettings.ValidationType=ValidationType.DTD

已解析并验证

因此,要允许根据 DTD 验证 XML 文档,您需要设置以下设置

using (XmlReader reader =
XmlReader.Create("people.xml", new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Parse,
    ValidationType = ValidationType.DTD
}))
{
    XDocument.Load(reader);
}

然而,DTD 仍然没有像模式那样可重用机制,因此每次解析文档时,都会解析和检索 DTD。您也无法轻松地使用您的程序拥有的 DTD 验证 XML 文档;您需要在添加 DTD 后重新解析文档(小心缩进!)。这与 XML 模式不同——XML 模式可以重用于解析一组文档。

传统上,没有开箱即用的缓存机制,许多人编写了某种缓存来存储他们的 XmlResolver。DTD 也通过 XmlResolver 解析,.NET 4.0 现在允许您对解析的流设置缓存策略。因此,如果 DTD 位于远程 PC 上(即,如果您的检索成本很高),您可以使用 XmlUrlResolver 的缓存策略解析 DTD

一个如何做到这一点的例子

using (XmlReader reader =
  XmlReader.Create("http://www.microsoft.com/en/us/default.aspx", 
  new XmlReaderSettings
  {
    DtdProcessing = DtdProcessing.Parse,
    ValidationType = ValidationType.DTD,
    XmlResolver = new XmlUrlResolver
    {
       CachePolicy = new RequestCachePolicy(RequestCacheLevel.CacheIfAvailable),
    }
}))
{
    XDocument.Load(reader);
}

如果您运行此程序,您将确切了解为什么 DTD 通常通过解析器在本地解析:应该托管 DTD 的网页返回“503 Page Unavailable”。这意味着您的 HTML 页面无法正确验证,除非您拥有自己的 DTD 副本,因为如果没有 DTD 的提及,任何文档都是不完整的,并且如果其中包含实体,您甚至无法在没有它的情况下加载它(例如 &nbsp;)。想象一下世界上每个浏览器或 HTML 处理器在每次加载 HTML 页面时都检索 DTD——同时有数百万人——这会花费多少?是否有足够的基础设施来处理这个问题?

结论

尽可能避免在文档中添加 DTD。将实体排除在文档之外(最简单的方法是使用 UTF-8),如果您需要验证,可以根据自己的方便使用 DTD 或 Schema。但您的程序应该决定根据哪个 DTD 或 Schema 进行验证。您不想根据文档自身的规则进行验证,而是根据您的程序期望的规则进行验证:您的程序需要指定它期望什么,它才不会关心文档是否认为自己有效。验证有其目的,其目的不是“确保文档根据其自身标准有效”,而这正是将 DTD 包含在文档中实际意味着什么。

最坏情况的示例

一个 Web 服务使用“customers.dtd”文件处理文件。它期望文件是符合客户 DTD 的有效 XML 文件。一位新程序员加入您的团队,看到该文件夹,并决定将符合“sales.dtd”文档类型的文件放入同一文件夹中。它们被处理了。Web 服务检查文档是否根据文档中提及的 DTD(在本例中为“sales.dtd”)有效。该 DTD 说这是一个有效文档,因此处理开始。但该服务现在每 10 分钟崩溃一次,尝试从文件夹中获取最旧的文件,因为内容不是它所期望的。如果程序根据客户的 DTD 而不是文档中提及的 DTD 进行验证,情况就会有所不同,并且很容易记录消息并移动文件。

Base URI 和 Stream,对 Schema 的影响

请参阅随附项目:StreamsSchemaSet, EmbeddedResourceResolver

流不包含告知读取器正在处理哪个文件名或 URI 的信息。您只有一串数据;它来自哪里,您无从得知。它可能来自 HTTP 请求、内存流或本地磁盘上的文件。流不知道它“在哪里”。但在某些情况下,“在哪里”很重要。例如,SchemaSet 类通过文件名或 URI 确定重复的 Schema。如果您正在使用流,则 Schema 无法告知 Schema 集它的 URI 是什么

假设我们编译了一个 SchemaSet。我们希望用它验证不同类型的文档,并且 Schema 的部分内容在不同格式之间重用。

首先通过流读取 XmlSchema,然后将它们添加到 SchemaSet 中,这无法正常工作

XmlSchemaSet set = new XmlSchemaSet();
XmlSchema schema = new XmlSchema();
using (FileStream fs = new FileStream("sale.xsd",
FileMode.Open))
{
    // reading the schema with default settings resolves the included schemas
    // properly, and does not trigger duplicates within the same root schema
    schema = XmlSchema.Read(fs, (o, e) => Console.WriteLine(e.Message));
}
set.Add(schema);
using (FileStream fs = new FileStream("client.xsd",
FileMode.Open))
{
    // however, adding an already included child schema twice gives a conflict
    schema = XmlSchema.Read(fs, (o, e) => Console.WriteLine(e.Message));
}
set.Add(schema);
set.Compile();

发生了什么?

  • 加载根模式 sale.xsd。该模式没有基 URI,因为它通过流加载。
  • 默认情况下,内部包含的模式使用 XmlUrlResolver 解析,因此它们确实具有模式已知的文件名。
  • 一个我们也想用作根模式,但已经通过“sale.xsd”包含的文件无法添加:示例中的流不知道位置,因此模式集无法确定它与已加载的模式文件是同一个。结果是抛出异常。

相反,如果我们将文件名传递给模式集,则包含的模式将像以前一样解析,并且所有模式都知道它们的位置。结果是第二个根模式不会被加载两次,并且由于重复声明而不会发生异常。

XmlSchemaSet set = new XmlSchemaSet();
XmlSchema schema = new XmlSchema();
// handing the SchemaSet filenames solves the
problem
set.Add(null, "sale.xsd");
set.Add(null, "client.xsd");
set.Compile();

如果您希望从嵌入式资源获取模式,这将带来问题,因此程序的普通用户将难以更改程序用于验证输入的模式。嵌入式资源只知道流,不知道文件名,因此您必须绕过它。您需要模式知道流的唯一 URI,最好的方法是使用您自己的 XmlResolver,将文件名或 URI 解析为嵌入式资源中的流,如下所示(您也可以为 DTD 执行此操作)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.IO;
using System.Resources;
using System.Diagnostics;
namespace EmbeddedResources
{
    /// <summary>
    /// Decorator class used to resolve xsd's from the embedded resources
    /// Limited capabilities: use a type in the same folder (namespace) as the resource
    /// to locate, will ignore all folders etc., will just look for
    /// the filename in the folder of the type T
    /// </summary>
    /// <typeparam name="T">Type to locate resources</typeparam>
    public class EmbeddedResourceUrlResolver<T> : XmlResolver
    {
        private readonly XmlResolver _resolver;
        private readonly string[] _schemes;
        /// <summary>
        /// Decorating constructor
        /// </summary>
        /// <param name="resolver"></param>
        /// <param name="schemes">array of Uri.UriScheme* constant entries</param>
        public EmbeddedResourceUrlResolver(XmlResolver resolver, params string[] schemes)
        {
            if (resolver == null) throw new ArgumentNullException("resolver");
            _resolver = resolver;
            _schemes = schemes;
        }
        /// <summary>
        /// Sets the credentials to use for resolving
        /// </summary>
        public override System.Net.ICredentials Credentials
        {
            set { _resolver.Credentials = value; }
        }
        /// <summary>
        /// Gets the <see cref="Stream" /> referenced by the uri
        /// </summary>
        /// <param name="absoluteUri"></param>
        /// <param name="role"></param>
        /// <param name="ofObjectToReturn"></param>
        /// <returns>a <see cref="Stream"/></returns>
        public override object GetEntity(Uri absoluteUri, string role, 
                                         Type ofObjectToReturn)
        {
            if (_schemes.Contains(absoluteUri.Scheme))
            {
                string filename = Path.GetFileName(
                    absoluteUri.ToString());
                Type locatorType = typeof(T);
                Stream stream = locatorType
                    .Assembly
                    .GetManifestResourceStream(locatorType, filename);
                if (stream == null)
                {
                    try
                    {
                        stream = (Stream)_resolver.GetEntity(absoluteUri, role, 
                                  ofObjectToReturn);
                    }
                    catch (MissingManifestResourceException missingException)
                    {
                        throw new MissingManifestResourceException(
                            string.Format(
                                "Embedded resource {0} could not be resolved " + 
                                "using type {1}. Full request was: {2}.",
                                filename, typeof(T), absoluteUri), 
                                missingException);
                    }
                    catch (IOException exception)
                    {
                        throw new MissingManifestResourceException(
                            string.Format(
                                "Embedded resource {0} could not be resolved " + 
                                "using type {1}. Full request was: {2}.", 
                                filename, typeof(T), absoluteUri), exception);
                    }
                    if (stream == null)
                    {
                        throw new MissingManifestResourceException(
                        string.Format("Embedded resource {0} could not be resolved " + 
                        "using type {1}. Full request was: {2}.",
                        filename, typeof(T), absoluteUri));
                    }
                }
                else
                {
                    Debug.WriteLine(filename + " was successfully resolved.");
                }
                return stream;
            }
            return _resolver.GetEntity(absoluteUri, role, ofObjectToReturn);
        }
    }
}

然后,您可以将 EmbeddedResourceResolver 提供一个文件名和类型(用于获取命名空间),并且将使用嵌入式资源(请注意,您可以重写它,将它找不到的请求传递给装饰的解析器)。再次禁用解析器将从应用程序可执行文件旁边的文件夹中的文件中加载它们。

这里有一些如何使用此类的示例代码

XmlSchemaSet set = new XmlSchemaSet();
XmlSchema schema = new XmlSchema();
set.XmlResolver = new
EmbeddedResourceUrlResolver<SchemaLocation>(new
XmlUrlResolver(), Uri.UriSchemeFile, Uri.UriSchemeHttp);
set.Add(null, "sale.xsd");
set.Add(null, "client.xsd");
set.Compile();

这些是我最常见的陷阱。现在,让我们看看 .NET 4.0 中发生了哪些变化,以便您能够自信地从 .NET 3.5 迁移到 .NET 4.0 XML。

.NET 4.0 中哪些功能已过时?

如前所述:XmlReader 不再有 ProbihitDtd,XML 序列化中不再使用 Evidence,也不再有 XmlValidatingReader

我所知道的以下类/方法/重载已过时

public class XmlConvert {
    public static String ToString(DateTime value);
}

public sealed class XmlReaderSettings {
    public Boolean ProhibitDtd { get; set; }
}

public class XmlTextReader : XmlReader, IXmlLineInfo, IXmlNamespaceResolver {
    public Boolean ProhibitDtd { get; set; }
}

public class XmlValidatingReader

public class XmlSerializer {
    public XmlSerializer(Type type, XmlAttributeOverrides overrides, 
           Type[] extraTypes, XmlRootAttribute root, String defaultNamespace, 
           String location, Evidence evidence);

    public static XmlSerializer[] FromMappings(XmlMapping[] mappings, 
           Evidence evidence);
}
 
public class XmlSerializerFactory{
    public XmlSerializer CreateSerializer(Type type, 
           XmlAttributeOverrides overrides, 
           Type[] extraTypes, XmlRootAttribute root, 
           String defaultNamespace, String location, 
           Evidence evidence);
}

System.XML 和相关命名空间中的更改

System.XML 命名空间中发生了什么变化?

XmlConvert

ToString (DateTime) 已过时。已将许多字符和 XML 字符串测试添加到 XmlConvert 类中

public class XmlConvert {
       
    [ObsoleteAttribute("Use XmlConvert.ToString() " + 
        "that takes in XmlDateTimeSerializationMode")]
    public static String ToString(DateTime value);

    public static Boolean IsNCNameChar(Char ch);
    public static Boolean IsPublicIdChar(Char ch);
    public static Boolean IsStartNCNameChar(Char ch);
    public static Boolean IsWhitespaceChar(Char ch);
    public static Boolean IsXmlChar(Char ch);
    public static Boolean IsXmlSurrogatePair(Char lowChar, Char highChar);
    public static String VerifyPublicId(String publicId);
    public static String VerifyWhitespace(String content);
    public static String VerifyXmlChars(String content);
}
  • IsNCNameChar:检查传入的字符是否是有效的非冒号字符类型。
  • IsPublicIdChar:如果参数中的字符是有效的公共 ID 字符,则返回传入的字符实例,否则返回空值。
  • IsStartNCNameChar:检查传入的字符是否是有效的起始名称字符类型。
  • IsWhitespaceChar:检查传入的字符是否是有效的 XML 空白字符。
  • IsXmlChar:检查传入的字符是否是有效的 XML 字符。
  • IsXmlSurrogatePair:检查传入的代理对字符是否是有效的 XML 字符。
  • VerifyPublicId:如果字符串参数中的所有字符都是有效的公共 ID 字符,则返回传入的字符串实例。
  • VerifyTOKEN:根据 W3C XML Schema Part 2: Data types 建议验证字符串是否为有效令牌。
  • VerifyWhitespace:如果字符串参数中的所有字符都是有效的空白字符,则返回传入的字符串实例。
  • VerifyXmlChars:如果字符串参数中的所有字符和代理对字符都是有效的 XML 字符,则返回传入的字符串,否则返回空值。

XmlReader

HasValueabstract 变为 virtual,因此获得了默认实现

public abstract class XmlReader : IDisposable {
    public (abstract) virtual Boolean HasValue { get; }
}

XmlReader.Create 现在还具有重载,可以以编程方式为流或读取器(任何本身没有基 URI 的东西,如文件名或 URI)设置基 URI。

XmlReaderSettings

ProbititDtd 已被 DtdProcessing 替换。

这个新设置还允许解析确实具有 DTD 的文档,但完全忽略它。

XmlResolver

请参阅随附项目:NonStreamXmlResolver

这个抽象类现在有一个 SupportsType 函数。

public abstract class XmlResolver {
    public virtual Boolean SupportsType(Uri absoluteUri, Type type);
}

此新功能允许 XmlResolver 返回其他内容,例如 XDocument。以下是一个实现此功能的类的示例

namespace NonStreamXmlResolver
{
    using System;
    using System.IO;
    using System.Xml;
    using System.Xml.Linq;
    /// <summary>
    /// XmlResolver that can resolve straight to XDocument
    /// instead of a stream. If anything asks an XDocument
    /// from the resolver, it will generate an XDocument from
    /// the stream it has gotten from the decorated resolver.
    /// </summary>
    public class XDocumentUrlResolver: XmlResolver
    {
        XmlResolver _resolver;
        public XDocumentUrlResolver(XmlResolver wrappedResolver)
        {
            _resolver = wrappedResolver;
        }
        public override System.Net.ICredentials Credentials
        {
            set { _resolver.Credentials = value; }
        }
        public override bool SupportsType(Uri absoluteUri, Type type)
        {
            if (type == typeof(XDocument)) return true;
            if (_resolver != null) return _resolver.SupportsType(absoluteUri, type);
            return false;
        }
        public override object GetEntity(Uri absoluteUri, string role, 
                               Type ofObjectToReturn)
        {
            if (_resolver != null && ofObjectToReturn == typeof(XDocument))
            {
                XDocument doc = XDocument.Load((Stream)_resolver.GetEntity(
                   absoluteUri, role, typeof(Stream)), LoadOptions.PreserveWhitespace);
                return doc;
            }
            if (_resolver != null)
                return _resolver.GetEntity(absoluteUri, role, ofObjectToReturn);
            throw new NotSupportedException("Can't resolve without an " + 
                   "underlying resolver");
        }
    }
}

当然,这种新能力的使用是有限的:XmlReader 不会突然返回 XDocument,这只是意味着您可以构建具有类似解析器机制的自己的框架,并从解析器类中获得更多的重用。

XmlTextReader

ProhibitDtdDtdProcessing 替换,以使其与 XmlReaderSettings 保持一致。

public class XmlTextReader : XmlReader, IXmlLineInfo, IXmlNamespaceResolver {
    [ObsoleteAttribute("Use DtdProcessing property instead.")]
    public Boolean ProhibitDtd { get; set; }
   
    public DtdProcessing DtdProcessing { get; set; }
}

XmlUrlResolver

请参阅随附项目:CachePolicy

XmlUrlResolver 现在具有用于缓存策略和代理的只写属性。

public class XmlUrlResolver : XmlResolver {
    public RequestCachePolicy CachePolicy { set; }
    public IWebProxy Proxy { set; }
}

请注意,这适用于 XmlUrlResolver,而不是 XmlResolver。一个是具体实现,另一个是其抽象基类。从 XmlResolver 继承不会让您获得 CachePolicy 或代理。

有关 XmlUrlResolver 的更多信息,请参见:http://msdn.microsoft.com/enus/library/system.xml.xmlurlresolver.aspx

您可以按如下所示设置代理和缓存策略

WebProxy proxy = new
WebProxy("https://:8080");
RequestCachePolicy policy = new
RequestCachePolicy(RequestCacheLevel.BypassCache);
XmlUrlResolver resolver = new XmlUrlResolver();
resolver.Proxy = proxy;
resolver.CachePolicy = policy;
using (XmlReader reader =
XmlReader.Create("people.xml", new XmlReaderSettings
{
    XmlResolver = resolver
}))
{
    XDocument.Load(reader);
}

XmlValidatingReader

此类别已过时,使其与其他 XmlReader 保持一致。现在像使用其他所有 XmlReader 类别一样使用 XmlReader.Create(不再需要了解具体实现)。

[ObsoleteAttribute("Use XmlReader created by XmlReader.Create() " + 
    "method using appropriate XmlReaderSettings instead. " + 
    "http://go.microsoft.com/fwlink/?linkid=14202")]
public class XmlValidatingReader : XmlReader, IXmlLineInfo, IXmlNamespaceResolver {
    public override XmlReaderSettings Settings { get; }
}

XmlWriterSettings

现在支持设置命名空间处理模式;这已在前面讨论过。

public sealed class XmlWriterSettings {
    public NamespaceHandling NamespaceHandling { get; set; }
}

DtdProcessing 和 NamespaceHandling

上面已讨论。

System.Xml.Serialization 命名空间中发生了什么变化?

XmlSerializer, XmlSerializerFactory

所有将 Evidence 作为参数的构造函数和创建序列化器的方法都已过时,并由一个不带此参数的新构造函数替换。

public class XmlSerializer {
    public XmlSerializer(Type type, XmlAttributeOverrides overrides, 
           Type[] extraTypes, XmlRootAttribute root, String defaultNamespace, 
           String location, Evidence evidence);
 
    public static XmlSerializer[] FromMappings(XmlMapping[] mappings, 
                                  Evidence evidence);
 
    public XmlSerializer(Type type, XmlAttributeOverrides overrides, Type[] extraTypes, 
           XmlRootAttribute root, String defaultNamespace, String location);
    }
 
    public class XmlSerializerFactory
    {
        public XmlSerializer CreateSerializer(Type type, XmlAttributeOverrides overrides, 
               Type[] extraTypes, XmlRootAttribute root, String defaultNamespace, 
               String location,Evidence evidence);
 
        public XmlSerializer CreateSerializer(Type type, XmlAttributeOverrides overrides, 
               Type[] extraTypes, XmlRootAttribute root, String defaultNamespace, 
               String location);
    }

System.Xml.Xsl 命名空间中发生了什么变化?

XslCompiledTransform

添加了一个新的 Transform 重载

public sealed class XslCompiledTransform {
    public void Transform(IXPathNavigable input, XsltArgumentList arguments,
                          XmlWriter results, XmlResolver documentResolver);
    }

System.Xml.Linq 命名空间中发生了什么变化?

SaveOptions

SaveOptions 现在允许指定删除或保留重复命名空间。SaveOptions 可以用作位标志并使用 |-运算符。

[FlagsAttribute]
public enum SaveOptions {
   OmitDuplicateNamespaces
}

这与 System.XML 中的 NamespaceHandling 枚举大致相同,但适用于 System.XML.Linq 并且仅用于写入文档。

XDocument, XElement 和 XStreamingElement

请参阅随附项目:XElementVsXStreamingElement

已添加从流加载和保存到流的重载。

public class XDocument : XContainer {
    public static XDocument Load(Stream stream, LoadOptions options);
    public static XDocument Load(Stream stream);
    public void Save(Stream stream);
    public void Save(Stream stream, SaveOptions options);
}

public class XElement : XContainer, IXmlSerializable {
    public static XElement Load(Stream stream, LoadOptions options);
    public static XElement Load(Stream stream);
    public void Save(Stream stream);
    public void Save(Stream stream, SaveOptions options);
}

public class XStreamingElement {
    public void Save(Stream stream);
    public void Save(Stream stream, SaveOptions options);
}

这些重载使用 UTF-8 读取器和写入器。

我们将展示一个使用流和 XStreamingElement 的示例。XStreamingElement 类似于普通的 Xelement,但有一个区别:内容是惰性求值的。这意味着 LINQ 查询或内部的 IEnumerable 只有在请求元素本身的值时才会被计算和处理(请注意,当您将其添加到 XElementXDocument 时,这已经发生了!)。这对于保持内存占用低,或者准备一个大型 XML 文件但最终不需要它,或者只需要一部分时特别方便。更多信息可以在 http://msdn.microsoft.com/en-us/library/system.xml.linq.xstreamingelement.aspx 找到,但现在让我们看看它的实际应用。

我们将使用一个小的辅助函数来在 LINQ 查询被枚举时输出内容

private static XElement CreateLineElement(stringline)
{
    Debug.WriteLine(line);
    return new XElement("line", line);
}

以下代码基于 LINQ 创建一个 XElement

string path = "line_input.txt";
Debug.WriteLine("start");
// the constructor of XElement enumerates
// the IEnumerable
XElement elem = new XElement("root",
        from line in ReadNextLine(path)
        select CreateLineElement(line));
 
Debug.WriteLine("after creation of XElement");
XDocument doc = new XDocument(elem);
Debug.WriteLine("after adding XElement to
XDocument");

输出显示 XElement 在其构造函数中枚举 LINQ 查询

start
Lorem ipsum dolor sit amet, consectetur adipiscing
elit.
Etiam lorem velit, elementum a pellentesque nec,
pharetra in ipsum.
Ut libero lorem, ultricies in auctor elementum,
vestibulum sed lacus.
Mauris consectetur quam sit amet libero pretium ut
dapibus libero ornare.
after creation of XElement
after adding XElement to XDocument

接下来,我们将尝试相同的方法,但使用 XStreamingElement

string path = "line_input.txt";
Debug.WriteLine("start");
// the constructor of XStreamingElement does
// not enumerate the IEnumerable yet
XStreamingElement elem = new XStreamingElement("root",
        from line in ReadNextLine(path)
        select CreateLineElement(line));
 
Debug.WriteLine("after creation of
XStreamingElement");
 
// XStreamingElement is enumerated when it is added
// to a non-streaming element or document
XDocument doc = new XDocument(elem);
Debug.WriteLine("after adding XStreamingElement to
XDocument");

这产生了以下输出

start
after creation of XStreamingElement
Lorem ipsum dolor sit amet, consectetur adipiscing
elit.
Etiam lorem velit, elementum a pellentesque nec,
pharetra in ipsum.
Ut libero lorem, ultricies in auctor elementum,
vestibulum sed lacus.
Mauris consectetur quam sit amet libero pretium ut
dapibus libero ornare.
after adding XStreamingElement to XDocument

这表明 XStreamingElement 的构造函数不会枚举 IEnumerable,但一旦将其添加到 XDocument(或 XElement),它就会被枚举。

保存 XDocument 则会生成一个 UTF-8 XML 文档

MemoryStream memStream = new MemoryStream();
doc.Save(memStream, SaveOptions.DisableFormatting);

结论

  • XStreamingElement 仅在请求其内容时才生成其内容(例如,如果您需要数据库访问,请在未请求其内容时保持数据库打开)。
  • XElement 在构造函数中生成其内容
  • XStreamingElement 添加到 XDocumentXElement 会生成其内容,因此,如果不需要,将一个充满 XStreamingElementXDocument 放置是毫无意义的,它们无论如何都会在文档的构造函数中计算出来。
  • 流重载使用 UTF-8 编码。

XNode

创建从 XNode 读取器的新重载,因此它也适用于任何派生类(例如 XElement)。

public abstract class XNode : XObject, IXmlLineInfo {
    public XmlReader CreateReader(ReaderOptions readerOptions);
}

结论

您的 .NET 3.5 XML 代码应该与新的 .NET 4.0 兼容。有新的处理 DTD 的方法,很高兴 DTD 终于得到了额外的关爱。重复的命名空间现在可以优雅地处理。更多的 XML 读取器已弃用,现在需要使用 XmlReader.Create,但这自 .NET 2.0 以来就已经是一个通用准则。一些陷阱仍然存在,使得使用 XML 有时比应有的更困难且更容易出错。XDocument 有或没有 XmlReader/Writer 之间的差异浮现在脑海中,XmlDocument 也是如此。例如,XStreamingElement 的文档仍然相当基础。

希望您喜欢阅读,如果您有任何问题或意见,请提出!

哦,感谢您阅读我的所有胡言乱语(您读到了最后!)

  1. 一些示例
  2. Altova XML Spy 不遵守此规定,但 .NET 3.5 和 .NET 4.0 的 XslCompiledTransform 默认设置遵守——XslCompiledTransform 是用于在 .NET 中执行 XSL 转换的类。
© . All rights reserved.