Enable XPath2 queries in System.Xml.XPath (XPathException: invalid token)

落花浮王杯 提交于 2021-01-28 03:56:09

问题


Microsofts System.Xml.XPath nuget-package, available for .NET 4.6, claims to support XPath 1.0 and 2.0. De documentation says descibes the namespace:

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.

After upgrading Visual Studio, upgrading and all my projects to framework version 4.6 I still can't get the simplest XPath-2.0 for-expression to work. According to the specification, they should work.

I can't imagine Microsoft claiming to support something it actually doesn't, so obviously I'm doing something wrong. How do I correctly use an XPath2 query?

[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"
}

P.S. What I actually want, is to get something like this to work:

    [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);
        }
    }

回答1:


Tomalek pointed out correctly:

  • XPath 2.0 is not supported in .NET (in System.Xml.XPath), period
  • The data model and the query language are separate things.

So I solved it by using a third party XPath 2 library, The XPath2 nuget package. This allows for expressions like

for $c in . return ../Writer[@ID=$c/WriterRef/@REFID]/Name

Note that I needed to use a relative path from book to writer. This does not work:

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

For future reference: this code works after installing the nuget pacage:

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);
            }
        }
    }
}



回答2:


I doubt that the XPath 2.0 query language really is supported.

But for something as simple as your requirement, an XPath 1.0 one-liner is sufficient.

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);

For queries that exceed the capabilities of XPath 1.0, try using LINQ.

In your extended example, what you want to do us link books to their authors. This is easily done in a LINQ join, like this:

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);

Now library is an IEnumerable<KeyValuePair<string,string>> with title/writer pairings.



来源:https://stackoverflow.com/questions/46805830/enable-xpath2-queries-in-system-xml-xpath-xpathexception-invalid-token

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!