Welcome to MSDN Blogs Sign in | Join | Help

Case-insensitive XPath in .NET

Do you know how you can perform case insensitive search using XPATH 1.0?

Today my colleage asked me how to do case insensitive XPATH in C#. I thought that I can give him an asnwer right away by searching web because I have experience with MSXML. MSXML 4.0 supports "string-compare()" function in its proprietary namespace called "urn:schemas-microsoft-com:xslt" that optionally takes a parameter whether you want to do case-insensitive string comparison or not. See here to find more information. 

After first attempt, however, I found that the namespace is not recognized in .NET Framework's XML classes. There was another alternative that we can use "translate()" function to translate "ABCDEFGHIJKLMNOPQRSTUVWXYZ" to "abcdefghijklmnopqrstuvwxyz" which used to be a common way to solve this problem for any standard XML parsers. But this is obviously wrong because English isn't the only language that this world use.

After a bit more investigation, I found that XPATH 2.0 now inlcuded many new string-related functions which provide option for case-insensitive comparison, but then soon I realized that .NET Framework 2.0 Beta 2 doesn't support XPATH 2.0. So then what is option?

Here is one way that I found useful. Tell me if you have better option.

I implemented a very simple custom XPATH function using XsltContext and IXsltContextFunction interface. The code here is not complete to help readibility. Full source code is available in the bottom of this post. The source code is based on .NET Framework 2.0 Beta 2.

using System;
using System.Xml;
using System.Xml.Xsl;
using System.Xml.XPath;

namespace CaseInsensitiveXPathTest
{
    class
Program
    {
        static void Main(string[] args)
        {
            XmlDocument doc = new XmlDocument();
            doc.LoadXml("<Lists><List Name=\"ABC\"/><List Name=\"abc\"/><List Name=\"123\"/></Lists>");
            CustomContext ctx = new CustomContext((NameTable)doc.NameTable);
            ctx.AddNamespace("s", http://shjin.local/);

            XmlNodeList nodes = doc.SelectNodes("//List[s:equals(@Name, \"ABC\")]", ctx);

            foreach (XmlNode node in nodes)
            {
                System.Diagnostics.Debug.WriteLine(node.OuterXml);
            }
        }
    }

    class CustomContext : System.Xml.Xsl.
XsltContext
    {
        public override IXsltContextFunction ResolveFunction(
            string prefix, string name, System.Xml.XPath.XPathResultType[] ArgTypes)
        {
            IXsltContextFunction resolvedFunction = null;

            if (this.LookupNamespace(prefix) == EqualsFunction.Namespace &&
                name == EqualsFunction.FunctionName)
            {
                resolvedFunction = new EqualsFunction();
            }

            return resolvedFunction;
        }

        private class EqualsFunction :
IXsltContextFunction
        {
            public const string Namespace = http://shjin.local/;
            public const string FunctionName = "equals";

            public object Invoke(XsltContext xsltContext, object[] args, System.Xml.XPath.XPathNavigator docContext)
            {
                if (args.Length != 2)
                {
                    throw new ArgumentException("equals() only takes two arguments");
                }

                string argument1 = GetStringFromInvokeArgument(args[0]);
                string argument2 = GetStringFromInvokeArgument(args[1]);

                bool result = string.Equals(argument1, argument2, StringComparison.CurrentCultureIgnoreCase);

                return result;
            }
        }
    }
}

I referenced a KB article called "HOW TO: Implement and Use Custom Extension Functions When You Execute XPath Queries in Vicual C# .NET". This article is a bit long and complecated. So I thought it would be helpful if I give a very short sample like above for helping other people's understanding.

See the following diagram to better understand the relationship between classes and interfaces:

UDF = User-defined function, UDV = User-defined variable

BTW I also see many people are struggling with how to deal with an XPATH that takes user input as a parameter especially when it can contain both double-quote and single-quote symbols since XPATH 1.0 standard doesn't really define a way to escape them when they appear together. The technique used here can be also used to sovle the issue as well. I may post another article for it if there are people interested in it as well.

The following is the full source code for this post:

using System;
using System.Xml;
using System.Xml.Xsl;
using System.Xml.XPath;

namespace CaseInsensitiveXPathTest
{
    class Program
    {
        static void Main(string[] args)
        {
            XmlDocument doc = new XmlDocument();
            doc.LoadXml("");

            CustomContext ctx = new CustomContext((NameTable)doc.NameTable);

            ctx.AddNamespace("s", "http://shjin.local/");

            XmlNodeList nodes = doc.SelectNodes("//List[s:equals(@Name, \"ABC\")]", ctx);

            foreach (XmlNode node in nodes)
            {
                System.Diagnostics.Debug.WriteLine(node.OuterXml);
            }
        }
    }

    class CustomContext : System.Xml.Xsl.XsltContext
    {
        #region Removed for bravity.

        public CustomContext()
            : base()
        {
        }

        public CustomContext(NameTable table)
            : base(table)
        {
        }

        public override bool Whitespace
        {
            get { return true; }
        }

        public override int CompareDocument(string baseUri, string nextbaseUri)
        {
            return 0;
        }

        public override bool PreserveWhitespace(System.Xml.XPath.XPathNavigator node)
        {
            return true;
        }

        #endregion

        public override IXsltContextFunction ResolveFunction(string prefix, string name, System.Xml.XPath.XPathResultType[] ArgTypes)
        {
            IXsltContextFunction resolvedFunction = null;

            if (this.LookupNamespace(prefix) == EqualsFunction.Namespace &&
                name == EqualsFunction.FunctionName)
            {
                resolvedFunction = new EqualsFunction();
            }

            return resolvedFunction;
        }

        public override IXsltContextVariable ResolveVariable(string prefix, string name)
        {
            return null;
        }


        private class EqualsFunction : IXsltContextFunction
        {
            public const string Namespace = "
http://shjin.local/";
            public const string FunctionName = "equals";

            private System.Xml.XPath.XPathResultType[] m_argTypes =
                new System.Xml.XPath.XPathResultType[] {
                    System.Xml.XPath.XPathResultType.Any,
                    System.Xml.XPath.XPathResultType.Any };

            #region IXsltContextFunction Members

            public System.Xml.XPath.XPathResultType[] ArgTypes
            {
                get
                {
                    return m_argTypes;
                }
            }

            private string GetStringFromInvokeArgument(object arg)
            {
                string stringValue;

                if (arg is string)
                {
                    stringValue = (string)(arg);
                }
                else if (arg is XPathNodeIterator)
                {
                    XPathNodeIterator i = (XPathNodeIterator)arg;

                    if (i.MoveNext() == true)
                    {
                        XPathNavigator navigator = i.Current;
                        stringValue = navigator.ToString();
                    }
                    else
                    {
                        throw new ArgumentException("One of argument doesn't expand as string.");
                    }
                }
                else
                {
                    throw new ArgumentException("equals() only support string or node path");
                }

                return stringValue;
            }

            public object Invoke(XsltContext xsltContext, object[] args, System.Xml.XPath.XPathNavigator docContext)
            {
                if (args.Length != 2)
                {
                    throw new ArgumentException("equals() only takes two arguments");
                }

                string argument1 = GetStringFromInvokeArgument(args[0]);
                string argument2 = GetStringFromInvokeArgument(args[1]);

                bool result = string.Equals(argument1, argument2, StringComparison.CurrentCultureIgnoreCase);

                return result;
            }

            public int Maxargs
            {
                get { return 2; }
            }

            public int Minargs
            {
                get { return 2; }
            }

            public System.Xml.XPath.XPathResultType ReturnType
            {
                get { return System.Xml.XPath.XPathResultType.Boolean; }
            }

            #endregion
        }
    }
}

This posting is provided "AS IS" with no warranties, and confers no rights.

Published Friday, July 22, 2005 10:30 PM by shjin
Filed under:

Comments

# XmlNode.SelectSingleNode non case-sensitive | hilpers

Wednesday, January 21, 2009 1:04 PM by XmlNode.SelectSingleNode non case-sensitive | hilpers

# Sunghwa Jin s Blog Case insensitive XPath in NET | alternative dating

Anonymous comments are disabled
 
Page view tracker