It's not that well documented, but Microsoft have a bunch of
extra functions you can use from XPath expressions within XSLT.
You can find the documentation on them
here.
What isn't made apparent is that none of them work with
XslTransform:
a subset of them will work with
XslCompiledTransform, and the others only work with MSXML
4.0 onwards.
This becomes obvious if you try and use these functions, or
if you read
this
blog post from the Microsoft XML team on the
XslCompiledTransform class.
Being the curious cat I am, I wondered if there was any way
to use the functions from within
XslTransform - if there was, I figured you could use the
functions in a BizTalk Map.
Short answer: No, but you can use them via Extension Objects
(I'll show you how later in this post).
Long Answer:
If you look at the source code for the
System.Xml.dll
assembly (either via
.NET
Reflector, or the
SSCLI,
or (even better) downloading the official source code using
NetMassDownloader) you can
look at how XSLT containing these functions is handled.
[
Note: Unfortunately, a lot of the "good
stuff" is actually implemented in the
System.Data.SqlXml.dll
assembly: this assembly contains the
System.Xml.Xsl.Runtime namespace in which
most of the logic is defined.
The assembly source is not included in the source you can
download using NetMassDownloader, so the only way of viewing it is via .NET
reflector, or the SSCLI. Interestingly, the
System.Data.SqlXml.dll assembly
no longer contains any SqlXml related code - it was all migrated to
System.Data.dll
and
Microsoft.Data.SqlXml.dll
with the release of .NET 2.0 from what I can see - all that's left is internal
XSLT code, so the assembly would now be better called
System.Xml.Xslt.dll]
As an example, we'll look at
ms:format-date: this will
format an XSD-style date/time using a given format string - pretty much the
same as using
DateTime.ToString(string) in .NET.
If you do a search for
format-date in the source code,
you find all of the implementation in the
System.Data.SqlXml.dll
assembly.
Firstly, you find a method called
ResolveFunction in the
QilGenerator
class (in the
System.Xml.Xsl.Xslt namespace):
(
Note:
Qil refers to the process of compiling
XSLT to MSIL and thence into a managed assembly via an
Abstract Syntax Tree
(AST) - it's an interesting topic and worthy of a post in it's own right! Give
me time...)
//
NOTE: DO NOT call QilNode.Clone() while executing this method since fixup nodes
cannot be cloned
QilNode IXPathEnvironment.ResolveFunction(string prefix, string
name, IList<QilNode>
args, IFocus env) {
...
else if
(name == "format-date" || name == "format-time")
{
FunctionInfo.CheckArity(/*minArg:*/1,
/*maxArg:*/3, name, args.Count);
bool fwdCompat = (xslVersion == XslVersion.ForwardsCompatible);
return f.InvokeMsFormatDateTime
(
/*datetime:*/f.ConvertToString(args[0]),
/*format: */1 < args.Count ?
f.ConvertToString(args[1]) : f.String(string.Empty),
/*lang: */2 < args.Count ?
f.ConvertToString(args[2]) : f.String(string.Empty),
/*isDate: */f.Boolean(name == "format-date")
);
}
What the
ResolveFunction method does is attempt to map a call to
an external function (defined in the
urn:schemas-microsoft-com:xslt namespace)
to a method defined in the
XsltFunctions class.
In this case, the method
InvokeMsFormatDateTime in the
XsltQilFactory
class (again in the
System.Xml.Xsl.Xslt namespace) is called to create an
early-bound method link (note that
XsltMethods.MSFormatDateTime returns a
System.Refelection.MethodInfo
object which can be used to call the method via reflection):
public QilNode
InvokeMsFormatDateTime(QilNode datetime, QilNode format, QilNode
lang, QilNode isDate) {
CheckString(datetime);
CheckString(format);
CheckString(lang);
CheckBool(isDate);
return XsltInvokeEarlyBound(QName("ms:format-date-time"),
XsltMethods.MSFormatDateTime, T.StringX, new QilNode[] { datetime, format, lang, isDate }
);
}
The
XsltMethods class contains this entry for
MSFormatDateTime:
internal static
class XsltMethods
{
...
public static
readonly MethodInfo
MSFormatDateTime = GetMethod(typeof(XsltFunctions), "MSFormatDateTime");
Which in turn points to the
MSFormatDateTime method in the
XsltFunctions
class (defined in the
System.Xml.Xsl.Runtime namespace):
namespace System.Xml.Xsl.Runtime {
using Res =
System.Xml.Utils.Res;
[EditorBrowsable(EditorBrowsableState.Never)]
public static
class XsltFunctions
{
private static readonly CompareInfo
compareInfo = CultureInfo.InvariantCulture.CompareInfo;
...
//
string ms:format-date(string datetime[, string format[, string language]])
// string ms:format-time(string datetime[, string format[,
string language]])
//
// Format xsd:dateTime as a date/time string for a given
language using a given format string.
// * Datetime contains a lexical representation of
xsd:dateTime. If datetime is not valid, the
// empty string is returned.
// * Format specifies a format string in the same way as for
GetDateFormat/GetTimeFormat system
// functions. If format is the empty string or not passed,
the default date/time format for the
// given culture is used.
// * Language specifies a culture used for formatting. If
language is the empty string or not
// passed, the current culture is used. If language is not
recognized, a runtime error happens.
public static
string MSFormatDateTime(string dateTime, string format, string lang, bool
isDate)
{
try {
int locale = GetCultureInfo(lang).LCID;
XsdDateTime xdt;
if (!XsdDateTime.TryParse(dateTime,
XsdDateTimeFlags.AllXsd | XsdDateTimeFlags.XdrDateTime | XsdDateTimeFlags.XdrTimeNoTz, out xdt)) {
return string.Empty;
}
SystemTime st = new
SystemTime(xdt.ToZulu());
StringBuilder sb = new
StringBuilder(format.Length + 16);
...
(interestingly, there's all sorts of good stuff in the
XsltFunctions
class, including the implementation of the XSLT
translate function, and other
functions performed by managed code when the XSLT is compiled to MSIL).
Phew! So it turns out that all the good stuff is defined in
the
XsltFunctions
class.
More importantly, the call to
ResolveFunction only
happens in
XslCompiledTransform.
So you can't use any of these functions from
XslTransform.
Which means there is no direct way to use them from the
BizTalk Mapper.
Calling extended XPath functions via Extension Objects
To call the extended functions from a BizTalk Map,
in theory all you need to do is reference them from a Script functoid (using a
Script
Type of "External Assembly"). Unfortunately if you try this,
you'll see that the
XsltFunctions class does not appear:

Those of you who were paying attention to the definition of
the
XsltFunctions
class may already understand why: you can't use Static Classes with Extension
Objects in a BizTalk Map.
The reason for this is that the extension objects are loaded
by BizTalk using this code:
object extension = Assembly.Load(assemblyName).CreateInstance(typeName);
CreateInstance will throw a
MissingMethodException if the
type passed in does not have a default constructor - which our static class does
not.
The BizTalk Mapper knows this, and so does not display
static classes in the list of classes in the script functoid.
As far as I am aware (and please correct me if you know of
another way) the only way to get around this would be to create a non-static
wrapper class round the methods you wanted.
For example if you did this:
namespace System.Data.SqlXmlHelper
{
public class Helper
{
public static string MSFormatDateTime(string
dateTime, string format, string lang, bool
isDate)
{
return XsltFunctions.MSFormatDateTime(dateTime,
format, lang, isDate);
}
}
}
Then you could use the above in a script functoid like this:

The other issue is that some of the functions aren't
defined in the
System.Data.SqlXml.dll assembly - they're in the
unmanaged MSXML dll (from v4.0 onwards) and are designed to be used only when
you're using MSXML to process the XSLT.
In order to save you the trouble of creating a wrapper
assembly for calling these functions, I've done it for you.
You'll find it (and the source code) in my next post.