Introduction
XPath Expressions are a powerful tool, however there are some limitations. Some things that you may need to do are not available yet in the specification, or you may need to do something slightly different from the specification, or there just isn't any way to do it except for using custom code. XPath Functions are a means to providing extra functionality to solve the problems that are not covered by any other means.
For this discussion, we will focus on the problem of performing case-insensitive searches in XPath expressions. The technique of creating and using custom functions and variables can certainly be applied to extend your XPath expression as necessary to solve other problems.
The Problem
XML is a case sensitive language, and although this can be a good thing, sometimes it provides for frustration. Validating the XML can be used to ensure proper formatting yet sometimes this is not possible, either because there is no schema available or you may not have control of the XML format, you just get what you get and have to make it work. When attempting to select nodes using a XPath expression, there is a difference between “*
and “*
. Given the XML snippet below, only one node would be returned for either of these queries.
<Addresses>
<Address type="Home">
<Street>100 Main St</Street>
<City>Uppercase</City>
<State>AZ</State>
<Zipcode>12345</Zipcode>
</Address>
<Address type="home">
<Street>100 Main St</Street>
<City>Lowercase</City>
<State>az</State>
<Zipcode>12345</Zipcode>
</Address>
<Address type="business">
<Street>1 Business Way</Street>
<City>Lowercase</City>
<State>AZ</State>
<Zipcode>12345</Zipcode>
</Address>
</Addresses>
But what if you need to find all the nodes, regardless of casing? One way to do this type of matching would be to iterate through a node list and filter the results as below:
XPathDocument doc = new XPathDocument("Test.xml");
XPathNavigator nav = doc.CreateNavigator();
XPathNodeIterator nodes = nav.Select("*//Address");
foreach(XPathNavigator node in nodes)
{
string attr = node.GetAttribute("type", "");
if(attr.ToLower() == "home" )
{
}
}
This is inefficient and cumbersome since you must retrieve the nodes, iterate through them, and filter out the ones that don't match. A better way would be to filter the results that are returned in the first place.
XsltContext
The .NET Framework supports the ability to add custom functions to your XPath expressions by specifying a XsltContext
. This abstract class provides a context for the XSLT processor to resolve any functions, variables and namespaces used in the XPath expression. When deriving a class from XsltContext
you must implement four methods and one property. The two most important methods are ResolveFunction
and ResolveVariable
. Although they have a purpose and usage during my explorations I have not found a usage for the remaining methods and properties.
public override IXsltContextFunction ResolveFunction(string prefix,
string name, XPathResultType[] ArgTypes)
public override IXsltContextVariable ResolveVariable(string prefix, string name)
ResolveFunction
When using an XPath expression that contains a function, such as below, the processor must be able to resolve the function in its context.
"*//Address[compare(string(@type),'home')]"
The ResolveFunction
method is used to return an implementation of IXsltContextFunction
that corresponds to the name that is passed in. The prefix parameter is of course the namespace prefix that may be associated with the function name. The ArgTypes
parameter is an array of the types for each parameter being used in the function. In the case of the above function it will have two elements of type string
.
public override IXsltContextFunction ResolveFunction(string prefix,
string name, XPathResultType[] ArgTypes)
{
IXsltContextFunction func = null;
switch(name)
{
case "compare":
func = new CompareFunction();
break;
default:
break;
}
return func;
}
IXsltContextFunction
The ResolveFunction
method above returns an implementation of the IXsltContextFunction
. This interface has one method and four properties that must be implemented. I have not found a case where the properties were used, though ReturnType
is accessed for each parameter in the function. The Invoke
method is what is called during processing of the XPath expression.
public object Invoke(XsltContext xsltContext, object[] args,
XPathNavigator docContext)
{
if(args.Length != 2)
throw new ApplicationException("Two arguments must be
provided to compare function.");
string Arg1 = args[0].ToString();
string Arg2 = args[1].ToString();
if(String.Compare(Arg1, Arg2, true) == 0)
return true;
else
return false;
}
This method is where the necessary work for the function takes place. In this case, after verifying that both parameters are available, a case insensitive comparison is made. If the string
s match, the method returns true
so that the node is included on the nodeset for the select.
XPathDocument doc = new XPathDocument("Test.xml");
XPathNavigator nav = doc.CreateNavigator();
CustomContext ctx = new CustomContext();
string XPath =string.Format("*//Address[compare(string(@type),'{0}')]", type);
XPathExpression exp = nav.Compile(XPath);
exp.SetContext(ctx);
XPathNodeIterator nodes = nav.Select(exp);
ResolveVariable
If you have used a variable in your XPath expression, such as this...
XPathExpression exp = nav.Compile("*//Address[compare(string(@type))=$value]");
... it will be resolved by calling the ResolveVariable
method of the XsltContext
derived class.
public override IXsltContextVariable ResolveVariable(string prefix,
string name)
{
return new CustomVariable(name);
}
A CustomVariable
class, which implements IXsltCustomVariable
is returned. IXsltCustomVariable
interface has only one method to implement. The Evaluate
method is called at runtime to retrieve the value of the specified variable.
public object Evaluate(XsltContext xsltContext)
{
XsltArgumentList args = ((CustomContext)xsltContext).ArgumentList;
return args.GetParam(Name, "");
}
The variable can then be used in the IXsltContextFunction.Invoke
:
public object Invoke(XsltContext xsltContext, object[] args,
XPathNavigator docContext)
{
string Value =
((CustomContext)xsltContext).ArgumentList.GetParam("value", "").ToString();
string Arg1 = args[0].ToString();
if(String.Compare(Arg1, Value, true) == 0)
return true;
else
return false;
}
A difference between using variables in your XPath expression and not is when the IXsltContextFunction.Invoke
method is called. When using variables, this method is called while iterating over the node-set, however when not using variables it is called when the Select
is executed.
Conclusion
This article has demonstrated using a custom XPath function to solve a simple problem. You should be able to use the techniques to extend the capabilities of XPath in your application.
History
- 28th August, 2006: Initial post