【问题标题】:Enable XPath2 queries in System.Xml.XPath (XPathException: invalid token)在 System.Xml.XPath 中启用 XPath2 查询(XPathException:无效令牌)
【发布时间】:2017-10-18 08:28:50
【问题描述】:

Microsoft 的 System.Xml.XPath nuget-package,可用于 .NET 4.6,声称支持 XPath 1.0 和 2.0。 De documentation 说描述了命名空间:

System.Xml.XPath 命名空间包含定义用于导航和编辑 XML 信息项的游标模型的类,作为 XQuery 1.0 和 XPath 2.0 数据模型的实例。

在升级 Visual Studio、升级和我的所有项目到框架版本 4.6 之后,我仍然无法让最简单的 XPath-2.0 for-expression 工作。根据specification,它们应该可以工作。

我无法想象微软声称支持它实际上不支持的东西,所以很明显我做错了什么。 如何正确使用 XPath2 查询?

[TestMethod]
public void TestXPath2()
{
    // The System.Xml.XPath namespace contains the classes that define a cursor model for navigating and editing XML information items as instances of the 
    // XQuery 1.0 and XPath 2.0 Data Model.

    var expression = "for $x in /Root/Foo/Bar return $x";
    var compiledExpression = System.Xml.XPath.XPathExpression.Compile(expression); 
    // throws XPathException: "for ... has an invalid token"
}

附: 我真正想要的是让这样的事情起作用:

    [TestMethod]
    public void TestLibraryForCustomer1()
    {
        string xmlFromMessage = @"<Library>
            <Writer ID=""writer1""><Name>Shakespeare</Name></Writer>
            <Writer ID=""writer2""><Name>Tolkien</Name></Writer>
            <Book><WriterRef REFID=""writer1"" /><Title>King Lear</Title></Book>
            <Book><WriterRef REFID=""writer2"" /><Title>The Hobbit</Title></Book>
            <Book><WriterRef REFID=""writer2"" /><Title>Lord of the Rings</Title></Book>
             </Library>"; 

        var titleXPathFromConfigurationFile = "./Title"; 
        var writerXPathFromConfigurationFile = "for $curr in . return /Library/Writer[@ID=$curr/WriterRef/@REFID]/Name";

        var library = ExtractBooks(xmlFromMessage, titleXPathFromConfigurationFile, writerXPathFromConfigurationFile).ToDictionary(b => b.Key, b => b.Value);

        Assert.AreEqual("Shakespeare", library["King Lear"]);
        Assert.AreEqual("Tolkien", library["The Hobbit"]);
        Assert.AreEqual("Tolkien", library["Lord of the Rings"]);
    }

    [TestMethod]
    public void TestLibraryForCustomer2()
    {
        string xmlFromMessage = @"<Library>
                <Writer ID=""writer1"">
                    <Name>Shakespeare</Name>
                    <Book><Title>Sonnet 18</Title></Book>
                </Writer>
                <Writer ID=""writer2"">
                    <Name>Tolkien</Name>
                    <Book><Title>The Hobbit</Title></Book>
                    <Book><Title>Lord of the Rings</Title></Book>
                </Writer>
            </Library>";

        var titleXPathFromConfigurationFile = "./Title";
        var writerXPathFromConfigurationFile = "../Name";

        var library = ExtractBooks(xmlFromMessage, titleXPathFromConfigurationFile, writerXPathFromConfigurationFile).ToDictionary(b => b.Key, b => b.Value);

        Assert.AreEqual("Shakespeare", library["Sonnet 18"]);
        Assert.AreEqual("Tolkien", library["The Hobbit"]);
        Assert.AreEqual("Tolkien", library["Lord of the Rings"]);
    }


    public IEnumerable<KeyValuePair<string,string>> ExtractBooks(string xml, string titleXPath,  string writerXPath)
    {
        var library = XDocument.Parse(xml);
        foreach(var book in library.Descendants().Where(d => d.Name == "Book"))
        {
            var title = book.XPathSelectElement(titleXPath).Value;
            var writer = book.XPathSelectElement(writerXPath).Value;
            yield return new KeyValuePair<string, string>(title, writer);
        }
    }

【问题讨论】:

  • “XQuery 1.0 和 XPath 2.0 数据模型”是对相关标准的引用 - 这并不意味着支持 xpath 2 导航。我无法立即找到文档中解释仅支持 xpath 1.0 的位置;如果可以的话,我会把它放在答案中

标签: c# .net xml xpath xpath-2.0


【解决方案1】:

Tomalek 指出正确:

  • .NET(在 System.Xml.XPath 中)不支持 XPath 2.0,句号
  • 数据模型和查询语言是分开的。

所以我通过使用第三方 XPath 2 库 XPath2 nuget package 解决了这个问题。这允许像

这样的表达式
for $c in . return ../Writer[@ID=$c/WriterRef/@REFID]/Name

请注意,我需要使用从书到作者的相对路径。这不起作用

# does not work due to the absolute path
for $c in . return /Library/Writer[@ID=$c/WriterRef/@REFID]/Name

供将来参考:此代码在安装 nuget 包后有效:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Wmhelp.XPath2;

namespace My.Library
{
    [TestClass]
    public class WmhelpTests
    {
        [TestMethod]
        public void LibraryTest()
        {
            string xmlFromMessage = @"<Library>
                <Writer ID=""writer1""><Name>Shakespeare</Name></Writer>
                <Writer ID=""writer2""><Name>Tolkien</Name></Writer>
                <Book><WriterRef REFID=""writer1"" /><Title>King Lear</Title></Book>
                <Book><WriterRef REFID=""writer2"" /><Title>The Hobbit</Title></Book>
                <Book><WriterRef REFID=""writer2"" /><Title>Lord of the Rings</Title></Book>
            </Library>";

            var titleXPathFromConfigurationFile = "./Title";
            var writerXPathFromConfigurationFile = "for $curr in . return ../Writer[@ID=$curr/WriterRef/@REFID]/Name";

            var library = ExtractBooks(xmlFromMessage, titleXPathFromConfigurationFile, writerXPathFromConfigurationFile).ToDictionary(b => b.Key, b => b.Value);

            Assert.AreEqual("Shakespeare", library["King Lear"]);
            Assert.AreEqual("Tolkien", library["The Hobbit"]);
            Assert.AreEqual("Tolkien", library["Lord of the Rings"]);
        }


        [TestMethod]
        public void TestLibraryForCustomer2()
        {
            string xmlFromMessage = @"<Library>
                <Writer ID=""writer1"">
                    <Name>Shakespeare</Name>
                    <Book><Title>Sonnet 18</Title></Book>
                </Writer>
                <Writer ID=""writer2"">
                    <Name>Tolkien</Name>
                    <Book><Title>The Hobbit</Title></Book>
                    <Book><Title>Lord of the Rings</Title></Book>
                </Writer>
            </Library>";

            var titleXPathFromConfigurationFile = "./Title";
            var writerXPathFromConfigurationFile = "../Name";

            var library = ExtractBooks(xmlFromMessage, titleXPathFromConfigurationFile, writerXPathFromConfigurationFile).ToDictionary(b => b.Key, b => b.Value);

            Assert.AreEqual("Shakespeare", library["Sonnet 18"]);
            Assert.AreEqual("Tolkien", library["The Hobbit"]);
            Assert.AreEqual("Tolkien", library["Lord of the Rings"]);
        }


        public IEnumerable<KeyValuePair<string, string>> ExtractBooks(string xml, string titleXPath, string writerXPath)
        {
            var library = XDocument.Parse(xml);
            foreach (var book in library.Descendants().Where(d => d.Name == "Book"))
            {
                var title = book.XPath2SelectElement(titleXPath).Value;
                var writer = book.XPath2SelectElement(writerXPath).Value;
                yield return new KeyValuePair<string, string>(title, writer);
            }
        }
    }
}

【讨论】:

  • 与使用 LINQ 相比,这是一个额外的外部依赖项和更多代码。为什么不使用 LINQ?
  • @Tomalek 能够配置 XML 结构。每个客户都会发送不同结构的文件。从 XML 实体到我的应用程序类的实际映射是按客户配置的。
  • 这很公平。
【解决方案2】:

我怀疑是否真的支持 XPath 2.0 查询语言。

但是对于像您的要求这样简单的事情,XPath 1.0 单行就足够了。

string xml = @"<Root>
    <Foo ID=""foo1"">One</Foo>
    <Foo ID=""foo2"">Two</Foo>
    <Bar><FooRef REFID=""foo2"" /></Bar>
    </Root>";
var doc = XDocument.Parse(xml);
var matchingFoo = doc.XPathSelectElement("/Root/Foo[@ID = //Bar/FooRef/@REFID]");

Assert.AreEqual("Two", matchingFoo.value);

对于超出 XPath 1.0 功能的查询,请尝试使用 LINQ。

在您的扩展示例中,您希望我们将书籍与其作者联系起来。这很容易在 LINQ 连接中完成,如下所示:

var xmlFromMessage = @"<Library>
    <Writer ID=""writer1""><Name>Shakespeare</Name></Writer>
    <Writer ID=""writer2""><Name>Tolkien</Name></Writer>
    <Book><WriterRef REFID=""writer1"" /><Title>King Lear</Title></Book>
    <Book><WriterRef REFID=""writer2"" /><Title>The Hobbit</Title></Book>
    <Book><WriterRef REFID=""writer2"" /><Title>Lord of the Rings</Title></Book>
     </Library>"; 

var libraryDoc = XDocument.Parse(xmlFromMessage);

var library = from title in libraryDoc.Descendants("Title")
    join writer in libraryDoc.Descendants("Writer")
    on title.Parent.Element("WriterRef").Attribute("REFID").Value equals writer.Attribute("ID").Value
    select new KeyValuePair<string, string>(title.Value, writer.Value);

现在library 是一个IEnumerable&lt;KeyValuePair&lt;string,string&gt;&gt;,具有标题/作者配对。

【讨论】:

  • 也许你应该让你的例子更清楚,那么?与其描述你想用什么实现来实现一个无法解释的目标,你可以描述你想实现什么目标,并让实现开放供辩论(也就是 XY 问题)。 .NET 不支持 XPath 2.0。使用 LINQ,或使用 XPath 1.0 的替代方法。 /Root/Foo[@ID = //Bar/FooRef/@REFID] 选择 all &lt;Foo&gt; 具有匹配 @REFID 的节点。这可以在第二步中过滤到您想要的特定内容。
  • 对此评论 +1。 “.Net 不支持 XPath 2.0。”这是我使用的答案。我会尝试一些第三方库。 (仍然:NuGet 包描述建议它这样做:提供定义光标模型的类,用于导航和编辑可扩展标记语言 (XML) 信息项作为 XQuery 1.0 和 XPath 2.0 数据模型的实例。
  • 数据模型和查询语言是分开的。不过,我在您的代码中看不到任何迹象表明内置工具不能用于执行您想要的操作。如果我一开始就知道你想做什么,我什至可以提出一种方法。
  • “.Net 不支持 XPath 2.0。”错误的。你的意思是,微软的 XPath 产品不支持 XPath 2.0。在更广泛的生态系统中,如果您准备超越 Microsoft,肯定有可在 .NET 中运行的 XPath 2.0(和 3.0/3.1)实现。
  • @MichaelKay 当然,这正是我的意思。
猜你喜欢
  • 2016-04-17
  • 2018-06-14
  • 1970-01-01
  • 1970-01-01
  • 2012-04-09
  • 2018-10-25
  • 1970-01-01
  • 2017-03-09
  • 1970-01-01
相关资源
最近更新 更多