God only knows why, but the .NET framework only includes support for XSLT 1.0. This makes it difficult, but not impossible, to use the more recent 2.0 version: a number of external libraries exist that can help us achieve that.
I wanted to make this easier for us developers, namely:
-
To have a common interface for abstracting typical functionality – transform some XML with some XSLT;
-
Be able to inject parameters;
-
Be able to inject custom extension functions.
I first set out to define my base API:
[Serializable]
public abstract class XsltProvider
{
public abstract String Transform(String xml, String xslt, XsltExtensionEventArgs args);
public abstract Single Version { get; }
}
[Serializable]
public sealed class XsltExtensionEventArgs
{
public XsltExtensionEventArgs()
{
this.Extensions = new Dictionary<String, Object>();
this.Parameters = new HashSet<XsltParameter>();
}
public IDictionary<String, Object> Extensions
{
get;
private set;
}
public ISet<XsltParameter> Parameters
{
get;
private set;
}
public XsltExtensionEventArgs AddExtension(String @namespace, Object extension)
{
this.Extensions[@namespace] = extension;
return this;
}
public XsltExtensionEventArgs AddParameter(String name, String namespaceUri, String parameter)
{
this.Parameters.Add(new XsltParameter(name, namespaceUri, parameter));
return this;
}
}
The XsltProvider class only defines a version and a transformation method. This transformation method receives an XML and an XSLT parameters and also an optional collection of extension methods and parameters. There’s a singleton instance to make it easier to use, since this class really has no state.
An implementation using .NET’s built-in classes, for XSLT 1.0, is trivial:
[Serializable]
public sealed class DefaultXsltProvider : XsltProvider
{
public static readonly XsltProvider Instance = new DefaultXsltProvider();
public override Single Version
{
get { return 1F; }
}
public override String Transform(String xml, String xslt, XsltExtensionEventArgs args)
{
using (var stylesheet = new XmlTextReader(xslt, XmlNodeType.Document, null))
{
var arg = new XsltArgumentList();
foreach (var key in args.Extensions.Keys)
{
arg.AddExtensionObject(key, args.Extensions[key]);
}
foreach (var param in args.Parameters)
{
arg.AddParam(param.Name, param.NamespaceUri, param.Parameter);
}
var doc = new XmlDocument();
doc.LoadXml(xml);
var transform = new XslCompiledTransform();
transform.Load(stylesheet);
var sb = new StringBuilder();
using (var writer = new StringWriter(sb))
{
var results = new XmlTextWriter(writer);
transform.Transform(doc, arg, results);
return sb.ToString();
}
}
}
}
For XSLT 2.0, we have a number of options. I ended up using Saxon-HE, an open-source and very popular library for .NET and Java. I installed it through NuGet:
Here is a possible implementation on top of my base class:
[Serializable]
public sealed class SaxonXsltProvider : XsltProvider
{
public static readonly XsltProvider Instance = new SaxonXsltProvider();
public override Single Version
{
get { return 2F; }
}
public override String Transform(String xml, String xslt, XsltExtensionEventArgs args)
{
var processor = new Processor();
foreach (var key in args.Extensions.Keys)
{
foreach (var function in this.CreateExtensionFunctions(args.Extensions[key], key))
{
processor.RegisterExtensionFunction(function);
}
}
var document = new XmlDocument();
document.LoadXml(xslt);
var input = processor.NewDocumentBuilder().Build(document);
var xsltCompiler = processor.NewXsltCompiler();
var xsltExecutable = xsltCompiler.Compile(input);
var xsltTransformer = xsltExecutable.Load();
foreach (var parameter in args.Parameters)
{
xsltTransformer.SetParameter(new QName(String.Empty, parameter.NamespaceUri, parameter.Name), CustomExtensionFunctionDefinition.GetValue(parameter.Parameter));
}
using (var transformedXmlStream = new MemoryStream())
{
var dataSerializer = processor.NewSerializer(transformedXmlStream);
xsltTransformer.InputXmlResolver = null;
xsltTransformer.InitialContextNode = processor.NewDocumentBuilder().Build(input);
xsltTransformer.Run(dataSerializer);
var result = Encoding.Default.GetString(transformedXmlStream.ToArray());
return result;
}
}
private IEnumerable<ExtensionFunctionDefinition> CreateExtensionFunctions(Object extension, String namespaceURI)
{
foreach (var method in extension.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static).Where(x => x.IsAbstract == false))
{
yield return new CustomExtensionFunctionDefinition(method, namespaceURI, extension);
}
}
class CustomExtensionFunctionDefinition : ExtensionFunctionDefinition
{
private readonly String namespaceURI;
private readonly MethodInfo method;
private readonly Object target;
public CustomExtensionFunctionDefinition(MethodInfo method, String namespaceURI, Object target)
{
this.method = method;
this.namespaceURI = namespaceURI;
this.target = target;
}
public override XdmSequenceType[] ArgumentTypes
{
get
{
return this.method.GetParameters().Select(x => this.GetArgumentType(x)).ToArray();
}
}
private XdmSequenceType GetArgumentType(ParameterInfo parameter)
{
return new XdmSequenceType(GetValueType(parameter.ParameterType), XdmSequenceType.ONE);
}
internal static XdmAtomicValue GetValue(Object value)
{
if (value is String)
{
return new XdmAtomicValue(value.ToString());
}
if ((value is Int32) || (value is Int32))
{
return new XdmAtomicValue(Convert.ToInt64(value));
}
if (value is Boolean)
{
return new XdmAtomicValue((Boolean)value);
}
if (value is Single)
{
return new XdmAtomicValue((Single)value);
}
if (value is Double)
{
return new XdmAtomicValue((Double)value);
}
if (value is Decimal)
{
return new XdmAtomicValue((Decimal)value);
}
if (value is Uri)
{
return new XdmAtomicValue((Uri)value);
}
throw new ArgumentException("Invalid value type.", "value");
}
internal static XdmAtomicType GetValueType(Type type)
{
if (type == typeof(Int32) || type == typeof(Int64) || type == typeof(Int16))
{
return XdmAtomicType.BuiltInAtomicType(QName.XS_INTEGER);
}
if (type == typeof(Boolean))
{
return XdmAtomicType.BuiltInAtomicType(QName.XS_BOOLEAN);
}
if (type == typeof(String))
{
return XdmAtomicType.BuiltInAtomicType(QName.XS_STRING);
}
if (type == typeof(Single))
{
return XdmAtomicType.BuiltInAtomicType(QName.XS_FLOAT);
}
if (type == typeof(Double))
{
return XdmAtomicType.BuiltInAtomicType(QName.XS_DOUBLE);
}
if (type == typeof(Decimal))
{
return XdmAtomicType.BuiltInAtomicType(QName.XS_DECIMAL);
}
if (type == typeof(Uri))
{
return XdmAtomicType.BuiltInAtomicType(QName.XS_ANYURI);
}
throw new ArgumentException("Invalid value type.", "value");
}
public override QName FunctionName
{
get { return new QName(String.Empty, this.namespaceURI, this.method.Name); }
}
public override ExtensionFunctionCall MakeFunctionCall()
{
return new CustomExtensionFunctionCall(this.method, this.target);
}
public override Int32 MaximumNumberOfArguments
{
get { return this.method.GetParameters().Length; }
}
public override Int32 MinimumNumberOfArguments
{
get { return this.method.GetParameters().Count(p => p.HasDefaultValue == false); }
}
public override XdmSequenceType ResultType(XdmSequenceType[] argumentTypes)
{
return new XdmSequenceType(GetValueType(this.method.ReturnType), XdmSequenceType.ONE);
}
}
class CustomExtensionFunctionCall : ExtensionFunctionCall
{
private readonly MethodInfo method;
private readonly Object target;
public CustomExtensionFunctionCall(MethodInfo method, Object target)
{
this.method = method;
this.target = target;
}
public override IXdmEnumerator Call(IXdmEnumerator[] arguments, DynamicContext context)
{
var args = new List<Object>();
foreach (var arg in arguments)
{
var next = arg.MoveNext();
var current = arg.Current as XdmAtomicValue;
args.Add(current.Value);
}
var result = this.method.Invoke(this.target, args.ToArray());
var value = CustomExtensionFunctionDefinition.GetValue(result);
return value.GetEnumerator() as IXdmEnumerator;
}
}
}
You can see that this required considerable more effort. I use reflection to find all arguments and the return type of the passed extension objects and I convert them to the proper types that Saxon expects.
A simple usage might be:
//sample class containing utility functions
class Utils
{
public int Length(string s)
{
//just a basic example
return s.Length;
}
}
var provider = SaxonXsltProvider.Instance;
var arg = new XsltExtensionEventArgs()
.AddExtension("urn:utils", new Utils())
.AddParameter("MyParam", String.Empty, "My Value");
var xslt = "<?xml version='1.0'?><xsl:transform version='2.0' xmlns:utils='urn:utils' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'><xsl:template match='/'>Length: <xsl:value-of select='utils:Length(\"Some Text\")'/> My Parameter: <xsl:value-of select='@MyParam'/></xsl:template></xsl:transform>";
var xml = "<?xml version='1.0'?><My><SampleContents/></My>";
var result = provider.Transform(xml, xslt, arg);
Here I register an extension object with a public function taking one parameter and also a global parameter. In the XSLT I output this parameter and also the results of the invocation of a method in the extension object. The namespace of the custom function (Length) must match the one registered in the XsltExtensionEventArgs instance.
As always, hope you find this useful!