ChatGPT解决这个技术问题 Extra ChatGPT

how to use XPath with XDocument?

There is a similar question, but it seems that the solution didn't work out in my case: Weirdness with XDocument, XPath and namespaces

Here is the XML I am working with:

<?xml version="1.0" encoding="utf-8"?>
<Report Id="ID1" Type="Demo Report" Created="2011-01-01T01:01:01+11:00" Culture="en" xmlns="http://demo.com/2011/demo-schema">
    <ReportInfo>
        <Name>Demo Report</Name>
        <CreatedBy>Unit Test</CreatedBy>
    </ReportInfo>
</Report>

And below is the code that I thought it should be working but it didn't...

XDocument xdoc = XDocument.Load(@"C:\SampleXML.xml");
XmlNamespaceManager xnm = new XmlNamespaceManager(new NameTable()); 
xnm.AddNamespace(String.Empty, "http://demo.com/2011/demo-schema");
Console.WriteLine(xdoc.XPathSelectElement("/Report/ReportInfo/Name", xnm) == null);

Does anyone have any ideas? Thanks.

See the other answer below, it doesn't work as the XPath 1.0 implementation can't cope with an empty prefix
As other said here, don't use an empty prefix when adding a Namespace to the [XmlNamespaceManager]. I'm just adding this comment in case anybody wants to see a small code example with a document that has several [xmlns] attributes, with and without a suffix. See here: stackoverflow.com/a/38272604/5838538

Q
Quppa

If you have XDocument it is easier to use LINQ-to-XML:

var document = XDocument.Load(fileName);
var name = document.Descendants(XName.Get("Name", @"http://demo.com/2011/demo-schema")).First().Value;

If you are sure that XPath is the only solution you need:

using System.Xml.XPath;

var document = XDocument.Load(fileName);
var namespaceManager = new XmlNamespaceManager(new NameTable());
namespaceManager.AddNamespace("empty", "http://demo.com/2011/demo-schema");
var name = document.XPathSelectElement("/empty:Report/empty:ReportInfo/empty:Name", namespaceManager).Value;

I'd say it's hard to say linq to be easier than xpath in most cases. For example in this case the LINQ equivalent is not really equivalent as it would also get "Name" nodes under other nodes (which aren't there now but could be added by later changes to the format of the file). However your solution is surely the right one.
NOTE: the using System.Xml.XPath; is pretty important because the XPathSelectElement is an extension method. Don't do as i did and ignore that part ;)
XPath is still helpful in that it allows you to contextualize your parent child relationships. E.g. if you wanted to get to /Banana/Banana/Banana instead of getting every Banana
"empty" is a bit misleading and confusing here. You can use anything except, with XPath, String.Empty (as the asker discovered). "demo" would be more appropriate to the example.
R
Richard Schneider

XPath 1.0, which is what MS implements, does not have the idea of a default namespace. So try this:

XDocument xdoc = XDocument.Load(@"C:\SampleXML.xml");
XmlNamespaceManager xnm = new XmlNamespaceManager(new NameTable()); 
xnm.AddNamespace("x", "http://demo.com/2011/demo-schema");
Console.WriteLine(xdoc.XPathSelectElement("/x:Report/x:ReportInfo/x:Name", xnm) == null);

Your answer implies that XPath 2.0, in contrast of XPath 1.0 "*has" an idea" of a default namespace. I am not aware of such new XPath feature (we are talking XPath here, not XSLT or XQuery). Therefore, could you, please, explicitly mention in your answer what you are implying?
I think what he is getting at here is that if you have a document which defines a namespace, your xpath must include qualified elements, i.e. you cannot do xnm.AddNamespace(string.Empty, "demo.com/2011/demo-schema"); and then xdoc.XPathSelectElement("/Report/ReportInfo/Name", xnm) - the result always comes out null
B
Bernhard

you can use the example from Microsoft - for you without namespace:

using System.Xml.Linq;
using System.Xml.XPath;
var e = xdoc.XPathSelectElement("./Report/ReportInfo/Name");     

should do it


This sample works only because the document has no default namespace. but the OPs document contains a default namespace "xmlns=..." and do the same with xpath it is not supported. you have always to specify an suffix which ist not empty.
k
kux

To work w/o default namespace suffix, I automatically expand the path.

Usage: SelectElement(xdoc.Root, "/Report/ReportInfo/Name");

private static XElement SelectElement(XElement startElement, string xpathExpression, XmlNamespaceManager namespaceManager = null) {
    // XPath 1.0 does not have support for default namespace, so we have to expand our path.
    if (namespaceManager == null) {
        var reader = startElement.CreateReader();
        namespaceManager = new XmlNamespaceManager(reader.NameTable);
    }
    var defaultNamespace = startElement.GetDefaultNamespace();
    var defaultPrefix = namespaceManager.LookupPrefix(defaultNamespace.NamespaceName);
    if (string.IsNullOrEmpty(defaultPrefix)) {
        defaultPrefix = "ᆞ";
        namespaceManager.AddNamespace(defaultPrefix, defaultNamespace.NamespaceName);
    }
    xpathExpression = AddPrefix(xpathExpression, defaultPrefix);
    var selected = startElement.XPathSelectElement(xpathExpression, namespaceManager);
    return selected;
}

private static string AddPrefix(string xpathExpression, string prefix) {
    // Implementation notes:
    // * not perfect, but it works for our use case.
    // * supports: "Name~~" "~~/Name~~" "~~@Name~~" "~~[Name~~" "~~[@Name~~"
    // * does not work in complex expressions like //*[local-name()="HelloWorldResult" and namespace-uri()='http://tempuri.org/']/text()
    // * does not exclude strings like 'string' or function like func()
    var s = Regex.Replace(xpathExpression, @"(?<a>/|\[@|@|\[|^)(?<name>\w(\w|[-])*)", "${a}${prefix}:${name}".Replace("${prefix}", prefix));
    return s;
}

If anyone has a better solution to find element and attribute names, feel free to change this post.