How do I resolve <xsl:import> and <xsl:include> elements with relative paths when using xsltc.exe XslCompiledTransforms?
Asked Answered
C

3

7

As part of our web application's build process, I have set up our XSLT stylesheets to be built with Microsoft's xsltc.exe compiler whenever we run a full compile. During local development this has worked great, as the code is compiled and hosted in the same location. However, once this was put on the build server, problems arose.

The build server will compile the XSLT stylesheets just like I do locally, but then a script runs that deploys the compiled code to our internal staging web server. Once these binaries have moved from where they were compiled, the relative paths in <xsl:import> and <xsl:include> elements no longer resolve correctly, causing exceptions that look like this when the XSLT stylesheets are ran.

Could not find a part of the path 'e:\{PATH}\xslt\docbook\VERSION'.
    at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
    at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath)
    at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options, String msgPath, Boolean bFromProxy)
    at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize)
    at System.Xml.XmlUrlResolver.GetEntity(Uri absoluteUri, String role, Type ofObjectToReturn)
    at System.Xml.Xsl.Runtime.XmlQueryContext.GetDataSource(String uriRelative, String uriBase)

Here's a general idea of the code as it stands now:

var xslt = new XslCompiledTransform();
xslt.Load(typeof(Namespace.XslTransforms.CompiledXsltStylesheet));
xslt.Transform("input.xml", "output.xml");

Right now I'm using the XslCompiledTransform.Load() method with a single 'Type' parameter to bring in the xsltc.exe-based pre-compiled XSLT stylesheets. I can tell from the stack trace that the .NET framework is using the XmlUrlResolver to try to resolve the actual location of these external stylesheets, but I don't see a way to provide an overridden implementation of XmlResolver where I could pass in a new baseUri that points to where these stylesheets live on the web server.

I assume I can resolve this by no longer pre-compiling with xsltc.exe and loading the XSLT stylesheets via XmlReaders, since that will let me use the other XslCompiledTransform.Load() methods which have a parameter where I could provide my own XmlResolver implementation. However, I like the pre-compilation option for syntax validation and performance, so I don't want to give it up unless I absolutely have to.

Is there a way to use xsltc.exe to pre-compile these XSLT stylesheets, yet still provide a way to explicitly state the baseUri for relative path resolution of <xsl:include> and <xsl:import> elements at runtime?

Corpsman answered 24/1, 2012 at 18:3 Comment(4)
Are the imported/included stylesheets not being deployed along with the binary (to the 'internal staging web server')?Dermatoid
They are, but they're in a different directory than where they were compiled.Corpsman
If you mimic the directory structure of the staging web server on the build server (where you're compiling the stylesheet), will that make a positive difference?Dermatoid
Forcing the file system to be in a particular format for XSLT is a pretty steep requirement that I can't require. I'd rather eat the performance hit. However, that may work for someone else making a XSLT web service in a SOA environment.Corpsman
C
5

After a lot of playing around with this, I found out that I was right that the code I provided automatically uses the System.Xml.XmlUrlResolver to resolve the <xsl:include> and <xsl:import> relative paths at run-time. However, the use of the XmlUrlResolver is not bound to the System.Xml.XslCompiledTransform when it is placed in a binary by xsltc.exe. The XmlResolver is actually chosen by the XmlResolver property on the System.Xml.XmlReaderSettings on the System.Xml.XmlReader that performs the transformation at run-time. Once I set my own custom XmlResolver on the XsltReaderSettings I was using, I was able to control relative path resolution.

If you want to override this XmlResolver as I did, the following code can be used as a guide:

var customXmlResolver = new SomeCustomXmlResolver();  // Derives from XmlResolver
var xmlReaderSettings = new XmlReaderSettings {
  XmlResolver = customXmlResolver
};

var xslt = new XslCompiledTransform();
xslt.Load(typeof(Namespace.XslTransforms.CompiledXsltStylesheet));

using (var xmlReader = XmlReader.Create("input.xml", xmlReaderSettings)) {
  using (var xmlWriter = XmlWriter.Create("output.xml")) {
    xslt.Transform(xmlReader, null, xmlWriter, customXmlResolver);
  }
}

I am still using xsltc.exe to compile my XSLT stylesheets, but when I load these compiled stylesheets on the web server, the injected SomeCustomXmlResolver rewrites the paths in overridden ResolveUri() and GetEntity() methods so that the referenced files that live in the <xsl:include> and <xsl:import>-based relative paths can be found. As an added bonus, by adding the same XmlResolver to the end of the Transform() method, document() operations in the XML will also have their relative paths resolved correctly.

Corpsman answered 4/4, 2012 at 3:59 Comment(3)
You must be very confused. The XmlReader in your code is used only for reading the "input.xml" -- not for loading the XSLT stylesheet. When the xslt.Load() method is executed, it has no reference to any XmlReader at all. If there is any problem with performing xsl:import and xsl:include, the Load() method raises an exception -- and this doesn't happen in your case -- without using any reference to the XmlReader.Tinsley
I am not confused Dimitre. I have working code, and stepping through a debugger I can clearly see these XSL elements' relative paths being passed through the XmlResolver I assign at run-time. <xsl:include href="../VERSION"/>, <xsl:include href="param.xsl"/>, <xsl:include href="../lib/lib.xsl"/>, etc. This is the right answer.Corpsman
Technetium: You have at least one of the imported /included stylesheets use the document() function with relative or empty URL -- this is the real problem, that you never explained. If none of the imported/included stylesheets reference the document() function, there would be no problem at all. I will find some free time in the next day to build examples proving this.Tinsley
T
2

Is there a way to use xsltc.exe to pre-compile these XSLT stylesheets, yet still provide a way to explicitly state the baseUri for relative path resolution of <xsl:include> and <xsl:import> elements at runtime?

Try to use:

XslCompiledTransform.CompileToType()

One of the arguments that this static method accepts is:

XmlResolver stylesheetResolver
Tinsley answered 25/1, 2012 at 3:15 Comment(3)
Ah yes! This looks very promising. I'll give it a go, and flag this as the correct answer once I have a chance to verify. The msdn documentation even states that xsltc.exe is a wrapper around this, so the big question that remains is if the XmlResolver caches the baseUri during compile-time or if the XmlResolver is ran again during run-time (which is both what I want and, based upon the stack trace, what I assume will happen).Corpsman
@Technetium: I think it doesn't make sense to fix the resolver so early at compile time. This should be what you are after.Tinsley
As I did in high school, I acted prematurely here because I was so excited. This is actually an incorrect answer. While Dimitre is right that the XmlResolver provided in this method is used to resolve <xsl:include> and <xml:import> elements when the XSLT stylesheet is compiled, it is not used again at run-time. The XmlResolver used at run-time is chosen by the XmlReaderSettings of the source XML file. See my answer for further details.Corpsman
D
0

I don't know if this breaks your system, but how about instead of

  1. compiling with xsltc.exe
  2. deploying the binary
  3. loading the binary with this Load()

you

  1. deploy the stylesheets, however many are required with the import/include directives
  2. load the main stylesheet with this Load(), specifying the resolver for the import/incldue

It appears you'll still get the benefit of a "compiled" stylesheet, at least in run-time.

Dermatoid answered 24/1, 2012 at 22:36 Comment(2)
It's definitely possible for me to access these XSLT stylesheets directly from the file system on the web server. Even with the main stylesheet compiled with xsltc.exe, they will HAVE to be there in order for the transform to function. However, unless there's something I don't understand about the Load() method you specified, that will compile the stylesheets each time they're loaded at run-time, which was one of things I was trying to avoid for performance reasons. That would also mean I'd have to degrade the syntax validation to Unit Test status.Corpsman
Oh, I thought the DLL was being loaded once, in the beginning. Yeah, that won't work then.Dermatoid

© 2022 - 2024 — McMap. All rights reserved.