It’s Always Something (#1): Wrestling with System.Xml.Xsl.XslTransform and the Document() function

It’s always something!

At Xtras, we have a rather sophisticated email broadcast system that we developed internally. Our system uses a set of tables in our SQL Server database that models the type of email, who it should go to, the mailing lists, etc. It loads each newsletter subscriber’s name and email address using a FOR XML EXPLICIT query from our SQL Server. It uses .XSL files to contain the HTML and/or Text content for each email, and then it uses a System.Xml.Xsl.XslTransform object to generate each HTML/Text email using a transform of the subscriber’s .XML fragment and the current newsletter’s .XSL file.  Once generated, it attempts to send using the SMTP component from /n software’s IP*Works!, and logs the success of failure into our database.

It is actually quite an elaborate and powerful system, and my hat’s off to Bill who developed it. However, it isn’t perfect. First, there is very little user interface; "the cobbler’s children go barefoot" as they say.  The second is our infrastructure doesn’t contemplate organization of .XSL files; there are literally hundreds all in the same directory.  But probably the biggest problem with it has been our approach to creating new email newsletters; we literally copy the XSL file from last newsletter issue and edit it, updating it for the current issue. 

Data driven?  Automated?  Modularized?  Yes it is, at least the parts that had to be are data-driven, automated, and modularized.  Like inserting the recipient’s email address and sending the emails.  But all other parts are manual!  Maybe you’ve gotten emails from us in the past where we’ve made errors like having two different dates on the same email in different places?  Now you know why…

Recently I grabbed a book on XSLT (Michael Kay’s XSLT: Programmer’s Reference; it’s actually quite good even though it’s four years old by now) and started learning how to write modular XSLT.  Of course it has taken longer than I planned so our next newsletter is late (our June newsletter for XDN; anyone notice its really late? Sorry…)  I managed to prepare a really elaborate stylesheet and pulled almost all of the content of the newsletter from data in our SQL Server database.  I then got the newsletter ready to go and passed it on to Bill to have him send it, and guess what?  It blew up his email system, giving code access security errors! 

Now you have to know Bill. He is very focused on completing his current projects, so Bill was not happy that my new .XSL blew up his "working" system!  But he understood that it would probaby take longer to rewrite than to solve the problem, and it is something we need to solve anyway.

The code access security error took us down the wrong path.  What I had added to the XSL file was a call to the XSLT document() function to load a second XML document (the newsletter content) from a URL, and we both thought XslTransform was telling us we needed to resolve a security problem in accessing a URL via HTTP.  Well, that wasn’t it; it was actually quite simple.  We needed an XmlResolver. More specifically, we needed an XmlUrlResolver.

So the moral of this long-winded story is, if you are using an XslTransform() and you add a reference to the document() function in  your .XSL file, you’ll need to add an XmlUrlResolver for it to work.  What follows is a self-contained simple example; copy into a class can call that class’ Exec method on a form click event to test (oh, and you’ll need to put an .XML and .XSL file at http://localhost/xslt/ for it to work):

Imports System.Xml
Imports System.Xml.XPath
Imports System.Xml.Xsl
Imports System.Text.Encoding
Public Class Test
   Private Const outFile As String = "XsltOutput.html"
   Public Sub Exec()
      Dim xslt As New XslTransform
      xslt.Load(FileUrl("XdnJun2004.xsl"))
      Dim data As New XPathDocument(FileUrl("Subscriber.xml"))
      Dim writer As New XmlTextWriter(outFile, UTF8)
      Dim xmlr As New XmlUrlResolver
      xslt.Transform(data, Nothing, writer, xmlr)
   End Sub
   Private Function FileUrl(ByVal filename As String) As String
      Dim baseUrl As String = "http://localhost/xslt/"
      Return CType(baseUrl & filename, String)
   End Function
End Class

I hope this post finds its way into Google’s index in hopes others with the same problem don’t have to spend as much time as we did trying resolving this problem.

Leave a Reply

Your email address will not be published.