In my last post, I talked about the new XslCompiledTransform class in .NET 2.0. It’s very cool, and seems to be very fast once it is loaded. There’s just problem.
What if you have more than one stylesheet?
I have a customer who builds their site using XML->HTML XSL Transformations, and they have around 200 stylesheets. We could spin up 200 instances of XslCompiledTransform, but that just seems silly. After all, since all the class is doing is compiling the XSL sheets to IL, we surely should be able to do that at compile time, or at least at run time and pass the compiled sheet in. Right?
Well, it doesn’t seem that way. My first clue was that XslCompiledTransform was sealed. That usually means that some juicy thing is in there that would be really, really, really useful if it wasn’t sealed. And in this case, that’s the truth. Because it has two methods that seem like they could be useful – CompileToQil
, and CompileIlFromQil
. The latter of which returns an XmlCommand
, which Transform just calls Execute on.
So it seems that if we could create the XmlCommand ourselves, we could just pass those in and have XslCompiledTransform execute them. Again, not the case, because all of those methods, and the underling object structure (from System.Data.SqlXml
) are all either internal or sealed. But, they don’t have access security around them, so let’s see what a little reflection can get us.
First, the path seems to be that on Load, we compile the XSL to QIL, which we can then compile down to IL from. So, my first goal was to see if I could get CompileToQil working. Using Lutz Roeder’s handy-dandy reflector, I see that CompileToQil is local to XslCompiledTransform, and takes a stylesheet, some XsltSettings, and a stylesheetResolver. Luckily, I can see that the code has some defaults, so let’s see what happens. First, we need to load the Assembly:
Assembly sqlXml = Assembly.LoadFrom(@"C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.Data.SqlXml.dll");
Next, I notice that CompileToQil is actually doing:
this.compilerResults = new System.Xml.Xsl.Xslt.Compiler(settings).Compile(stylesheet, stylesheetResolver, out this.qil);
So rather than spin up an XslCompiledTransform, let’s see if we can just create a Compiler and call Compile. First, we need to create the type and spin up an instance:
Type typClsCompiler = sqlXml.GetType("System.Xml.Xsl.Xslt.Compiler", true);
object oClsCompiler = Activator.CreateInstance(typClsCompiler, new Object[] { XsltSettings.Default }, null);
I used XsltSettings.Default, as that is what XslCompiledTransform used if it wasn’t passed in. Next, we’ll create a method info, and invoke it. Except…the last parameter in Reflector shows that QIL is returned as an out parameter:
I’ve done some reflection, but I’ve never had to deal with out params in an invoked method. Luckily, a man who is a camera had just the thing, and showed me the way, leading to this:
// set the third parameter modifier value to true to
// mark it as being an out parameter
ParameterModifier[] arrPmods = new ParameterModifier[1];
arrPmods[0] = new ParameterModifier(3);
arrPmods[0][0] = false; // not out
arrPmods[0][1] = false; // not out
arrPmods[0][2] = true; // out
// define the types of parameters in the target signature
// use a reference type for the out parameter
System.Type[] arrTypes = new System.Type[3];
arrTypes.SetValue(Type.GetType("System.Object"), 0);
arrTypes.SetValue(systemXml.GetType("System.Xml.XmlResolver"), 1);
arrTypes.SetValue(sqlXml.GetType("System.Xml.Xsl.Qil.QilExpression&"), 2);
MethodInfo miCompile = typClsCompiler.GetMethod("Compile", arrTypes, arrPmods);
// package our parameters
object[] arrParms = new object[3];
arrParms.SetValue(stylesheet, 0);
arrParms.SetValue(stylesheetResolver, 1);
// no need to set the third parameter -- it will be filled in
object results = miCompile.Invoke(oClsCompiler, arrParms);
// extract the value from the out parameter
object qilObject = arrParms[2];
Console.WriteLine("qilObject null? " + (qilObject == null));
Which, when run, promptly printed qilObject null? false
!
I don’t really believe it could be that easy, since it looks like I’m halfway to my goal. Just to confirm, I set a breakpoint and look at what qilObject really is:
Which gives me proof that I do indeed have a QIL object. So, with that out of the way, let’s see if we can get it compiled to IL. Looking back at Reflector, specifically the CompileIlFromQil
method, I see that to create the XmlCommand, we create an XmlILGenerator object and call Generate, passing in the QIL and the AssemblyName of the XsltSettings. Since we already have both of those, let’s see if we can just create that ourselves. First, I verify that XmlILGenerator is indeed internal:
So, let’s set up some reflection calls:
Type typClsXmlILGenerator = sqlXml.GetType("System.Xml.Xsl.XmlILGenerator", true);
object oClsXmlILGenerator = Activator.CreateInstance(typClsXmlILGenerator);
//We've now got the QIL. Let's compile it to IL
MethodInfo miGenerate = typClsXmlILGenerator.GetMethod("Generate");
object xmlCommand = miGenerate.Invoke(oClsXmlILGenerator,
new Object[] { qilObject, XsltSettings.Default.GetType().Assembly.GetName() });
Console.WriteLine("xmlCommand null? " + (xmlCommand == null));
which is much cleaner, because we aren’t dealing with out parameters. Notice the call to get the Assembly name for XsltSettings – in Reflector it shows a call to an AssemblyName property of XsltSettings, but I don’t have access to that here, so I’m just jumping to reflection to get it. So, now, we should have the IL generated and be ready for the next steps to inject it into XslCompiledTransform. First, let’s just run it to make sure:
Well then. A COMException in interop? Perhaps not what I was expecting, and probably won’t be easy to track down. I’m sure I can dive into it and see what comes out, but I’ve been at this for a couple of hours now, and think that this is a good stopping point.
So, what have we learned? It’s a shame that we have all of these internal and sealed classes, because the ability to precompile the XSL sheets seems like a very useful utility. We have some fallbacks, including create static members for each stylesheet, or creating a lazy loaded lookup table for XslCompiledTransform instances, but I’m going to keep playing with this to see how far it gets me.
Hello Cory,
I’ working on a project facing a similar problem (100s of XSLs in an application). I’ve completed it using a lookup-table for one instance per XSL used and just stumbled across this. I’m curious if you have succeeded in with the method you’ve started as it is extremely interesting so far! (although beyond my capabilities ;)
Great work!
Regards,
Tom
Hi Tom,
Unfortunately no, I haven’t had much time to revisit this. I think the lookup option to your XSLCompiledTransform instances is your best option at this point.
Cory
Hi Cory,
thank you for the quick response to my post (even tough the post itself has quite an age).
If we’ll have the time I might let someone look into it, if we come to anything I’ll drop you a line!
Thank you once again for the inspiration,
Best wishes,
Thomas
I’m not sure about timelines and such, but did XSLTC.EXE exist in the Windows SDK when you wrote this blog post?