Sunday 20 February 2011

Response.Filter - What? How? And Why?

Response.Filter allows you to change the HTML that is output to the client after .NET has processed everything. It essentially gives you a central place to make any last second changes.

So, what's the use of this? Well, there's actually quite a few uses. For example, say you've developed your web application for a test environment and so, all your hyperlinks look something like: http://www.example.com/Test/APage.aspx. Then, when you go to a live environment, you may want to change all of those links to http://www.example.com/Live/APage.aspx. Well, you could go through your entire application and change the links where necessary, or, you could just add a response filter and have that take care of it.

There's a few ways you can assign the filter but my personal favourite is to assign it within the Application_BeginRequest method within the global.asax file1. So, you end up with a method that looks like this:

void Application_BeginRequest(object sender, EventArgs e)
{
    HttpApplication app = sender as HttpApplication;
    if(app.Request.FilePath.EndsWith(".aspx"))
        app.Response.Filter = new Filter(app.Response.Filter);
}

We check to make sure that the FilePath is an actual .aspx page as every request dealt with by the application will pass through this method. That means every request for an image, javascript file, css file or any other file you can think of, will pass through here and the chances are, you'll only want to apply your logic when requesting aspx pages.

Now, we need to create the Filter class. The purpose of this class is to take the HTML that the application will create and modify it to what we need, then pass that back out to the client, so in this example, we want to search for "/Test/" and change it to "/Live/".

The Filter class should inherit from Stream and so, a simplistic version would be the following:

public class Filter : Stream
{
    Stream s;

    public Filter(Stream stream)
    {
        this.s = stream;
    }

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

    public override bool CanSeek
    {
        get { return s.CanSeek; }
    }

    public override bool CanWrite
    {
        get { return s.CanWrite; }
    }

    public override void Flush()
    {
        s.Flush();
    }

    public override long Length
    {
        get { return s.Length; }
    }

    public override long Position
    {
        get
        {
            return s.Position;
        }
        set
        {
            s.Position = value;
        }
    }

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

    public override long Seek(long offset, System.IO.SeekOrigin origin)
    {
        return s.Seek(offset, origin);
    }

    public override void SetLength(long value)
    {
        s.SetLength(value);
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        String text = System.Text.Encoding.UTF8.GetString(buffer, offset, count);
        text = text.Replace("/Test/", "/Live/");

        byte[] buff = System.Text.Encoding.UTF8.GetBytes(text);
        s.Write(buff, 0, buff.Length);  
    }
}



We've now ensured every "Test" link will change to a "Live" link, in a single, central location. This means that if in the future it needs to change again, it's a very simple change. As you can probably guess, this feature has the potential to make your life a whole lot easier and it opens up a few doors that previously, weren't available to us, well, not without a bit of work anyway. One big use that I've seen it used for is to reduce the amount of unnecessary white space that's sent down the wire, doing such a thing will decrease the amount data sent which should speed up download time.2

Now that you know how to use the Response.Filter, there's a couple of things that you should know before you start working with that property. These things have the potential to cause a few problems that are a little difficult to get around if you don't know what's going on, so I've listed the issues below.
  1. ASP.NET uses "chunking" to output pages. By this I mean, that the Write method of the Filter class defined above, will be called for every "chunk" of data, which I think is around 16kb. So, if the HTML that ASP.NET creates is bigger than 16kb, then you can sometimes see odd behaviour. Taking the above example, what if the "/Te" happened to be the last characters of one chunk and "st/" was the beginning of the next. Our simple replace wouldn't match and you'd still see the /Test/ link. If you do have pages bigger than 16kb then you really need to capture all the output, then perform your transformations, and then output everything, rather than outputting small pieces at a time. A fantastic implementation of this can be found here: http://west-wind.com/weblog/posts/72596.aspx.
  2. Using Response.Filter can cause serious problems with UpdatePanels unless you know what you're doing. In my previous blog post I showed you the format of the response text for an UpdatePanel callback, in it, it includes a Length property which indicates the length of the text being sent across the wire. If you then modify the text so it has more/less characters than it originally did, the actual length of the text won't match up with the value that indicated the length and the application will throw an error. I'll propose a method of fixing this problem in my next blog.
And that's about it, a very straight forward example of Response.Filter and some of it's uses. Enjoy!


1 For more information about global.asax, check out this MSDN article http://msdn.microsoft.com/en-us/library/2027ewzw.aspx
2 A good implementation of this can be found at http://www.codeproject.com/KB/aspnet/RemovingWhiteSpacesAspNet.aspx

No comments:

Post a Comment