XSLT Processing in .NET
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!