Welcome to MSDN Blogs Sign in | Join | Help

Introducing BAM Interceptor for Windows Workflow Foundation

Early release of BizTalk Server 2006 R2 is available now for some TAP customers. I expect more people will have chance to experience R2. See http://www.microsoft.com/presspass/press/2006/jun06/06-06EtEBusinessProcessPR.mspx for more information about how you can participate in TAP program.

BizTalk Server 2006 R2 enables support for .NET Framework 3.0 (formerly known as WinFX) and today I want to mention that BAM will expand its ability to capture business activities happing in Windows Workflow Foundation (WF) and Windows Communication Foundation (WCF. A.k.a. Indigo) through R2 release by providing BAM Interceptor for WF/WCF.

Customers will be able to write Interceptor Configuration (IC) in XML format to describe interception points and deploy the IC through BAM manager (BM.exe) and plug in interceptor by modifying application configuration file (app.config) to intercept business activity happing on application hosting WF/WCF.

Parameterized XPath in .NET

Is there anyone who wonder how you can perform an XPath query when you need to find a XML node with an attribute that contains single quote and double quote symbols together? For example,

<Sample Text="Sunghwa's thought about &quot;Matrix Revolution&quot;" />

The problem here is that XPath 1.0 standard doesn't mention how you can escape single quote or double quote in literal. According to the standard it just define literal as:

Literal ::= '"' [^"]* '"' | "'" [^'] "'"

Some of you may think that you can simply do this:

doc.SelectSingleNode("//Sample[@Text=\"Sunghwa's thought about &quot;Matrix Revolution&quot;\"]");

But the problem is, as defined in the XPath 1.0 standard, the literal string is any string enclosed by single quote symbol except the single quote itself or any string enclosed by double quote symbol except the double quote. So &quot; is interpreted as it is.

Some people didn't give up and use solution like below:

doc.selectSingleNode("//Sample[@Text=concat(\"Sunghwa's thought about \", '\"Matrix Revolution\"')]");

But I believe this isn't attractive solution to most of you.

XPath injection is also the same concern when you need to build your XPath based on user input like below:

doc.selectSingleNode("//User[@Name='" + userName + "']");

So what's solution here?

Before I decided to post this article, I actually found an article from Cazzu's blog which already implemented this, but I thought it would be interesting to show some points that I want to discuss in simple code rather than more complecated MVP XML Library. I recommend this library although I didn't have chance to use it.

For those who want to see the simple code rather than using the library, I wrote my own sample using the same technique I posted in my article about case-insensitive XPath.

The following is the sample code that shows how clean the code is after applying it.

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

ctx.AddParam("name", @" Sunghwa's thought about \"Matrix Revolution\"");

XmlNodeList nodes = doc.SelectNodes("//Sample[@Name = $name]", ctx);

The full source code is available below. This sample has been modifed from my previous posting's sample:

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=\"A'&quot;BC\"/><List Name=\"abc\"/><List Name=\"123\"/></Lists>");

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

            ctx.AddNamespace("s", "http://shjin.local/");
            ctx.AddParam("name", @"A'""BC");

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

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

    class CustomContext : System.Xml.Xsl.XsltContext
    {
        private XsltArgumentList m_argumentList = new XsltArgumentList();

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

        public void AddParam(string name, object parameter)
        {
            m_argumentList.AddParam(name, string.Empty, parameter);
        }

        public object GetParam(string name)
        {
            return m_argumentList.GetParam(name, string.Empty);
        }

        public object RemoveParam(string name)
        {
            return m_argumentList.RemoveParam(name, string.Empty);
        }

        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)
        {
            IXsltContextVariable contextVariable;

            object parameter = this.GetParam(name);

            if (parameter != null)
            {
                contextVariable = new ContextVariable(name, parameter);
            }
            else
            {
                contextVariable = null;
            }

            return contextVariable;
        }

        private class ContextVariable : IXsltContextVariable
        {
            private string m_name;
            private object m_parameter;

            public ContextVariable(string name, object parameter)
            {
                m_name = name;
                m_parameter = parameter;
            }

            #region IXsltContextVariable Members

            public object Evaluate(XsltContext xsltContext)
            {
                return m_parameter;
            }

            public bool IsLocal
            {
                get { return true; }
            }

            public bool IsParam
            {
                get { return true; }
            }

            public XPathResultType VariableType
            {
                get
                {
                    return XPathResultType.Any;                   
                }
            }

            #endregion
        }

        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.

Posted by shjin | 1 Comments
Filed under:

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.

Posted by shjin | 2 Comments
Filed under:
 
Page view tracker