Search code examples
c#.netxpath-1.0

Using an XPath expression within SelectNodes


I've seen this question where Dimitre Novatchev shows a way of replicating ends-with with an XPath 1.0 expression. However I am having trouble implementing it in context of within a SelectNodes call.

Previously I was using

XmlElement root = doc.DocumentElement;
XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
nsmgr.AddNamespace("x", root.NamespaceURI);    
XmlNodeList nodeList = doc.SelectNodes("//x:*[contains(name(.), '-notification')]", nsmgr);

Which returned all the nodes I wanted plus one I didn't which had an additional 's' on the end (has-more-notifications).

So I tried using Dimitre expression which gave me:

XmlNodeList nodeList = doc.SelectNodes("//x:*[substring(name(.), string-length(name(.)) - string-length('-notification') +1)]", nsmgr);

Which fails miserably giving me the root node of notification-data-response.

This is my first foray into XPath and it seems to be like regex - you either understand it or you don't.

How do I implement the expression so it returns only the nodes that end with -notification?

UPDATE

A sample of the input:

<?xml version="1.0" encoding="UTF-8"?>
<notification-data-response xmlns="http://checkout.google.com/schema/2" serial-number="16ceae10-a9f1-4ff0-a77b-c3407f2d684a">
    <notifications>
        <new-order-notification serial-number="653417067275702-00001-7">
        </new-order-notification>
        <order-state-change-notification serial-number="653417067275702-00005-1">
        </order-state-change-notification>
        <risk-information-notification serial-number="653417067275702-00005-5">
        </risk-information-notification>
        <authorization-amount-notification serial-number="653417067275702-00005-6">
        </authorization-amount-notification>
    </notifications>
    <continue-token>CP6u9NeQJxC2y72h-MiUARgG</continue-token>
    <has-more-notifications>false</has-more-notifications>
</notification-data-response>

Solution

  • you have to use local-name() function if you want to avoid namespace prefixes. Something like this should give you all nodes ending with -notification

    //node()[substring(local-name(), string-length(local-name()) - string-length('-notification')+ 1, string-length(local-name()))= '-notification']

    Ok .. I tested this here . you can verify too .. XPath is an open standard so all tools should be able to give similar response.

    http://www.xpathtester.com/test

    INPUT

     <?xml version="1.0"?>
    <notification-data-response xmlns:x="test">
        <TPI_ADDRESSES>
            <ISPRIMARY>Y</ISPRIMARY>
            <data1>
                <x:wewe-notification/>
            </data1>
            <data2>
                <x:wewe-notification/>
            </data2>
        </TPI_ADDRESSES>
    </notification-data-response>
    

    Output

    <?xml version="1.0" encoding="UTF-8"?>
    
    <root>
      <x:wewe-notification xmlns:x="test"/>
      <x:wewe-notification xmlns:x="test"/>
    </root>
    

    ignore the root node as it is generated to present a valid formatable response.

    Now let me break down and explain the xpath to you:

    "//" -- denotes that you are searching doing a wild search or we call full scan.. like you don't care at what level target node comes.

    "node()" -- is a reference to any and every node or the current node ....

    so now : "//node()" -- together denote that you are going to evaluate all the nodes in the xml.

    What do you evaluate ?

    the name -- you want to find if the name of the node contains "-notification" in it. For that you use the substring function in LHS

    substring(local-name(), string-length(local-name()) - string-length('-notification')+ 1, string-length(local-name()))

    RHS is you search string = '-notification'

    local-name() = gives the name of the node that you are evaluating at any point excluding prefix string-length() - length of the node name