Skip navigation

A Better Way to XSLT

I've been thinking a lot about code performance and optimization lately.  I want my websites to be fast, and it seems I'm always looking for ways to squeeze a little more performance out of my code.  I've looked a lot at the various caching mechanisms available in ASP.NET.  I've looked at database schema design.  I've even looked at the ways in which I construct large, complex strings - StringBuilder, anyone?

A recent project of mine required working with a lot of XML content.  This XML content had to be converted into HTML for output on many of the pages in the web site.  No problem, right?  I've done this so many times; I could do it in my sleep!  Read in the XML, load up the XSLT, and let the .NET Framework do its magic.  Problem solved.  Or is it?

As I said, I've been looking into performance lately.  It's kind of an obsession of mine.  An ASPX page that takes 1 or 2 seconds to load (not including time sending the rendered content from server to client) is like fingernails on a chalkboard to me.  Remember chalkboards?  Those green or black things at the front of a classroom?  Remember how dusty they would get by the end of the day or sometimes the end of the week?  Remember how nice they looked when your 3rd grade teacher had just washed them?  But I digress.

In .NET, XSLTs are handled by the XslCompiledTransform class ever since version 2.0 was released.  Now, I had never really thought that much of this class, but it says right there in the class name that it is compiled.  That means every time you create a new instance and Load an XSLT, the .NET Framework will compile the XSLT.  Compiling a resource is a CPU-intensive task, and when you are compiling the same resource on every page view, that can turn into a very expensive operation.  What I needed was a way to ensure that I only loaded a given XSLT once during the lifetime of my application.  For that purpose, I created an XslRepository - a class that would manage all of my XSLTs.

public class XsltRepository : IXsltRepository
{
    private static readonly object LockObect = new object();
    private static readonly Dictionary<string, XslCompiledTransform> XsltCache = 
new Dictionary<string, XslCompiledTransform>();

    public void AddXslt(string relativeWebPathToStylesheet)
    {
        if (string.IsNullOrEmpty(relativeWebPathToStylesheet)) return;
        var stylesheetUri = HostingEnvironment.MapPath(relativeWebPathToStylesheet);
        if (string.IsNullOrEmpty(stylesheetUri)) return;

        lock(LockObect)
        {
            var transform = new XslCompiledTransform();
            transform.Load(stylesheetUri);
            XsltCache.Add(relativeWebPathToStylesheet, transform);
        }
    }

    public XslCompiledTransform GetXslt(string relativeWebPathToStylesheet)
    {
        return XsltCache.ContainsKey(relativeWebPathToStylesheet) ? 
XsltCache[relativeWebPathToStylesheet] : null;
    }
}

Since this class uses a static dictionary to hold all the compiled transforms, I had to use a lock when adding to the dictionary.  In order to minimize the code that relies on this lock, I pre-filled the dictionary during the Application_Start event with all the XSLT files the site would need.

protected void Application_Start(object sender, EventArgs e)
{
    var xsltCachingService = new XsltRepository();
    xsltCachingService.AddXslt("~/xslt/SideMenu.xslt");
    xsltCachingService.AddXslt("~/xslt/FooterMenu.xslt");
    xsltCachingService.AddXslt("~/xslt/PrimaryMenu.xslt");
    xsltCachingService.AddXslt("~/xslt/LanguageMenu.xslt");
    xsltCachingService.AddXslt("~/xslt/Stockquote.xslt");
}

At this point, executing an XSLT is simply a matter of getting the compiled transform from the repository and then using it per usual.  Nice, eh?

One thing still didn't sit right with me. I didn't want to create a string in memory to hold the transformed content, only to then put that string into a Literal control.  That seemed like a waste of memory to me.  I don't need a copy of the content in a string; I want it written directly to the response.  Wouldn't it be more efficient if I could render the output directly into the response stream? Yes.  Yes it would.  To accomplish this, I created for myself a .NET server control called CompiledXslt.

[ToolboxData("<{0}:CompiledXslt runat=server />")]
public class CompiledXslt : Control
{
    private XslCompiledTransform Xslt { get; set; }

    private readonly IXsltRepository XsltRepository;

    private object DocumentSource { get; set; }

    public XsltArgumentList ArgumentList { get; set; }

    [Bindable(true)]
    [Category("Appearance")]
    [DefaultValue("")]
    [Localizable(false)]
    public string XsltPath
    {
        get { return ViewState["XsltPath"] as string ?? String.Empty; }
        set { ViewState["XsltPath"] = value; }
    }

    public void SetDocumentSource(IXPathNavigable xpathNavigable)
    {
        DocumentSource = xpathNavigable;
    }

    public void SetDocumentSource(string inputUri)
    {
        DocumentSource = inputUri;
    }

    public void SetDocumentSource(XmlReader xmlReader)
    {
        DocumentSource = xmlReader;
    }
 
    public CompiledXslt()
        : base()
    {
        XsltRepository = new XsltRepository();
        ArgumentList = new XsltArgumentList();
    }

    protected override void OnPreRender(EventArgs e)
    {
        base.OnPreRender(e);
        Xslt = XsltRepository.GetXslt(XsltPath);
    }

    protected override void Render(HtmlTextWriter writer)
    {
        if (Xslt == null) 
throw new InvalidOperationException(string.Format("Missing XSLT [{0}].", XsltPath));

        if (DocumentSource is IXPathNavigable)
        {
            Xslt.Transform((IXPathNavigable)DocumentSource, ArgumentList, writer);
        }
        else if (DocumentSource is XmlReader)
        {
            Xslt.Transform((XmlReader)DocumentSource, ArgumentList, writer);
        }
        else if (DocumentSource is string)
        {
            Xslt.Transform((string)DocumentSource, ArgumentList, writer);
        }
        else
        {
            var docSourceType = (DocumentSource != null) ? 
DocumentSource.GetType().ToString() : "null";
            throw new InvalidOperationException(
string.Format("Invalid Document Source [type: {0}].", docSourceType));
        }
    }
}

One thing you might notice is that my document source is an object. The XslCompiledTransform class accepts three different source types:  IXPathNavigable, XmlReader, and a string representing an input URI.  In order to maximize this control's reusability, I wanted the control to accept all three data types.  I did not want the hassle of maintaining three different properties for the document source, so I store it as an object and let the Render method perform the necessary type checking and casting before calling the XSLT's Transform method.

This control does exactly what I wanted it to do -writing directly to the response stream.  The Render method passes its HtmlTextWriter parameter into the Transform method, thereby eliminating the extra placeholder string and extra memory consumption that goes along with it.

As a design decision, this control throws an exception if you tell it to use an XSLT that is not already loaded into the repository.  I could see an argument for modifying the control to render the source XML as-is in this scenario.

So what's next?  This is a good first step, but there is still room for improvement.  For one thing, changes in the XSLT file won't be picked up by the application until the next time it starts.  There's room for some creative use of .NET's caching with file-based dependencies to get around that.  Another thing that could be improved: there's no easy way to pass in an XML document stored as a string to my CompiledXslt control; in its current implementation it will treat any string as an "inputURI".  There's plenty of room for extending this solution, but my mind is more at ease knowing that my code will perform better, using less memory and CPU.