views:

257

answers:

4

I've been trying to implement server-side XSLT transformations as an IIS HttpModule. My basic approach is to install a new filter at BeginRequest that diverts writes into a MemoryStream, and then at PreSendRequestContent to transform the document using XSLT and write it to the original output stream. However, even without performing the transformation I'm clearly doing something wrong as the HttpModule appears to work for the first page load and then I get no response from the server at all until I restart the application pool. With the transformation in place I get an empty page the first time and then no response. I'm clearly doing something stupid but this is the first C# code I'd written in years (and my first attempt at an HttpModule) and I have no idea what the problem might be. What mistakes am I making? (I've commented out the XSLT part in the code below and uncommented a line that writes the contents of the cache to the response.)

using System;
using System.IO;
using System.Text;
using System.Web;
using System.Xml;
using System.Xml.Xsl;

namespace Onyx {

    public class OnyxModule : IHttpModule {

        public String ModuleName {
            get { return "OnyxModule"; }
        }

        public void Dispose() {
        }

        public void Init(HttpApplication application) {

            application.BeginRequest += (sender, e) => {
                HttpResponse response = HttpContext.Current.Response;
                response.Filter = new CacheFilter(response.Filter);
                response.Buffer = true;
            };

            application.PreSendRequestContent += (sender, e) => {

                HttpResponse response = HttpContext.Current.Response;
                CacheFilter cache = (CacheFilter)response.Filter;

                response.Filter = cache.originalStream;
                response.Clear();

 /*               XmlReader xml = XmlReader.Create(new StreamReader(cache), new XmlReaderSettings() {
                    ProhibitDtd = false,
                    ConformanceLevel = ConformanceLevel.Auto
                });

                XmlWriter html = XmlWriter.Create(response.OutputStream, new XmlWriterSettings() {
                    ConformanceLevel = ConformanceLevel.Auto
                });

                XslCompiledTransform xslt = new XslCompiledTransform();
                xslt.Load("http://localhost/transformations/test_college.xsl", new XsltSettings() {
                    EnableDocumentFunction = true
                }, new XmlUrlResolver());
                xslt.Transform(xml, html); */

                response.Write(cache.ToString());

                response.Flush();

            };

        }


    }

    public class CacheFilter : MemoryStream {

        public Stream originalStream;
        private MemoryStream cacheStream;

        public CacheFilter(Stream stream) {
            originalStream = stream;
            cacheStream = new MemoryStream();
        }

        public override int Read(byte[] buffer, int offset, int count) {
            return cacheStream.Read(buffer, offset, count);
        }

        public override void Write(byte[] buffer, int offset, int count) {
            cacheStream.Write(buffer, offset, count);
        }

        public override bool CanRead {
            get { return cacheStream.CanRead; }
        }

        public override string ToString() {
            return Encoding.UTF8.GetString(cacheStream.ToArray());
        }

    }

}
+3  A: 

When you are done reading the data into your MemoryStream the position is at the end of the stream. Before sending the stream to the StreamReader/XmlReader you need to reset the position to 0.

stream.Position = 0;
/* or */
stream.Seek(0, SeekOrigin.Begin);
Mikael Svenson
The second version has the arguments in the wrong order. I've now modified the code to do that and I get the XSLT output once. The second time I load the page I get "'', hexadecimal value 0x1F, is an invalid character. Line 1, position 1" which is certainly a step forward.
Rich
Corrected my code.. that's what I get without intellisense ;) For further debugging you should dump your incoming stream to disk and examine that it is in fact valid xml. Could it be that it is gzipped?
Mikael Svenson
0x1F seems to be the BOM for your utf-8 encoded response. You can instruct UTF8Encoding to omit the BOM: http://msdn.microsoft.com/en-us/library/s064f8w2.aspx
Filburt
@Mikael: The problem was static content compression. When I turned that off it worked fine (although I moved the XSLT processing into the filter's "Write" method.)
Rich
+2  A: 

I'm a little surprised this works at all (even after resetting the stream's position). I poked around the HttpApplication code a bit, and though I don't fully understand it, it looks like you may be modifying the output stream too late in the request handling process.

If you still haven't figured this out, try attaching your second handler function to one of the events after PostReleaseRequestState - either UpdateRequestCache or PostUpdateRequestCache. Neither sounds especially suitable, but read on!

For some reason, the MSDN documentation for HttpApplication doesn't include PreSendRequestContent in its list of events, but Reflector shows that its handlers don't get called until HttpResponse.Flush.

If I'm reading the code right, Response.Flush calculates the content length before the handlers are called, so it thinks the response is empty when it gets to this code:

if (contentLength > 0L) {
    byte[] bytes = Encoding.ASCII.GetBytes(Convert.ToString(contentLength, 0x10) + "\r\n");
    this._wr.SendResponseFromMemory(bytes, bytes.Length);
    this._httpWriter.Send(this._wr);
    this._wr.SendResponseFromMemory(s_chunkSuffix, s_chunkSuffix.Length);
}

There are some alternate paths that may get called depending on your entry point and initial conditions, and that might explain why this works some of the time but not others. But at the end of the day you probably shouldn't be modifying the response stream once you're in Flush.

You're doing something a little unusual - you're not really filtering the response stream in the traditional sense (where you pass some bytes along to another stream), so you may have to do something a bit hackish to make your current design work.

An alternative would be to implement this using an IHttpHandler instead of a module - there's a good example here. It deals with transforming output from a database query, but should be easy to adapt to a file system data source.

Jeff Sternal
I've tried attaching the handlers to other events but to no avail. I think I might be somehow doing something wrong with streams though.In this case an HttpHandler implementation isn't the best option as I want to be able to transform files, WCF data services, the output of other applications and so on transparently. At the moment we're doing this using the URL rewriter to map external URLS to a PHP script that forwards requests to another endpoint and then transforms the responses using libxslt but the performance could be better and it's very far from transparent.
Rich
Blast - ah well, I'll leave this here to ward away anyone else that might be similarly misguided.
Jeff Sternal
+1  A: 

Even if you don't stick to the msdn example you should implement HttpApplication.EndRequest:

context.EndRequest += (sender, e) => {
    HttpResponse response = HttpContext.Current.Response;
    response.Flush();
};

cleaner

// ...

public void Init(HttpApplication application)
{
    // ...
    application.EndRequest += (new EventHandler(this.Application_EndRequest));
}

private void Application_EndRequest(object sender, EventArgs e)
{
    HttpApplication application = (HttpApplication)source;
    HttpContext context = application.Context;
    context.Current.Response.Flush();
}
Filburt
Ironically, this is how I started out but then changed to the lambda version because I thought it was cleaner.
Rich
A: 

Check out this project, maybe it suits your needs:

http://myxsl.net/

Max Toro