Background
Have you ever needed to access the output from a web page and modify it before it was sent to the client? When an ASP.Net web page contains several user controls, .aspx HTML content, data bound server controls, and content added by the code behind file, it can be time consuming to make changes that would impact all of these areas (especially when multiple pages are involved). Often it is useful to allow the page to be created, then simply access the output that is about to be sent to the client, but modify it in some way. The idea is to create a custom filter that the Response (HttpResponse class) object uses to write the content. The filter references a Stream object, so it is necessary to derive a custom class from the abstract Stream object. The Stream class has a Write( ) method which is where the code to modify the output should be placed. Once the class is created, the Response.Filter can be changed to use the custom filter, and the content can be modified by a single method before being sent to the client. This is very powerful concept when combined with regular expressions, which allow selective matching of content and replacements.
Implementation
Basic filtering
System.IO.Stream is an abstract class, so it it cannot be instantiated, and can only serve as a base class for other classes. The first step, therefore, is to create a derived class and implement the abstract methods of the System.IO.Stream class. Create a new class file in a project and replace its code with the following, which is a basic implementation of the derived class provided from the help for Response.Filter in Visual Studio.Net (converts all content to upper case):
using System;
using System.IO;
namespace ResponseFilters
{
public class UpperCaseFilter : Stream
// This filter changes all characters passed through it to uppercase.
{
private Stream _sink;
private long _position;
public UpperCaseFilter(Stream sink)
{
_sink = sink;
}
// The following members of Stream must be overriden.
public override bool CanRead
{
get { return true; }
}
public override bool CanSeek
{
get { return true; }
}
public override bool CanWrite
{
get { return true; }
}
public override long Length
{
get { return 0; }
}
public override long Position
{
get { return _position; }
set { _position = value; }
}
public override long Seek(long offset, System.IO.SeekOrigin direction)
{
return _sink.Seek(offset, direction);
}
public override void SetLength(long length)
{
_sink.SetLength(length);
}
public override void Close()
{
_sink.Close();
}
public override void Flush()
{
_sink.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
return _sink.Read(buffer, offset, count);
}
// The Write method actually does the filtering.
public override void Write(byte[] buffer, int offset, int count)
{
byte[] data = new byte[count];
Buffer.BlockCopy(buffer, offset, data, 0, count);
for (int i = 0; i < count; i++)
// Change lowercase chars to uppercase.
{
if (data[i] >= 'a' && data[i] <= 'z')
data[i] -= ('a'-'A');
}
_sink.Write(data, 0, count);
}
}
}
A page that wants to implement the given filter can then simply just replace the existing default filter with this in the Page_Load( ) method of the page:
private void Page_Load(object sender, System.EventArgs e)
{
Response.Filter = new ResponseFilters.UpperCaseFilter(Response.Filter);
}
After compiling and running the page, you will see that ALL of the page has been converted to upper case (including the META tags, HTML tags, etc). This demonstrates the power of this technique, imagine having to modify a series of pages that were all lower case in the .aspx page, code behind, user controls, etc. But if you now drag a ASP.Net button control unto the form, and simply hit the button to cause a postback, you will get an exception that says
The View State is invalid for this page and might be corrupted. What happened? If you go back and view the HTML source for the page, you will see that the __VIEWSTATE hidden field that manages viewstate for ASP.Net pages was also converted to all uppercase. This field stores values for server controls, and it uses Message Authentication Code (MAC) to determine if the data has been modified. The MAC is a value computed based on the original data and then added to the data. When the page is posted, the server can compute the MAC again and compare to the one sent to determine if the data has been modified. Guess what, its been modified to be all capital letters! So although this is powerful, it is too granular of an approach.
Regular Expressions to Control Modifications
The next step is to target changes to only particular portions of a page. The changes required to provide the custom modifications must be done to the Write( ) method of the class. ASP.Net appears to call this method for every 28k worth of data (and in some other instances as well). To ensure that matches are not missed across this boundary, it is necessary to buffer the information internally. This sample code requires that the page must end in </html> so that it knows when the last portion is being written so it can do the actual write of the locally buffered data. For example, the following code can be placed in the Write( ) method to convert instances of the text "test" with "newTest":
public override void Write(byte[] buffer, int offset, int count)
{
//Get a string version of the buffer
string szBuffer = System.Text.UTF8Encoding.UTF8.GetString(buffer, offset, count);
//Look for the end of the HTML file
Regex oEndFile = new Regex("</html>", RegexOptions.IgnoreCase);
if (oEndFile.IsMatch(szBuffer))
{
//Append the last buffer of data
oOutput.Append(szBuffer);
//Get back the complete response for the client
string szCompleteBuffer = oOutput.ToString();
Regex oRegEx = new Regex("test", RegexOptions.IgnoreCase);
if (oRegEx.IsMatch(szCompleteBuffer))
{
//Found string, so replace all occurences with the new value (use a non-case sensitive
// match)
string newBuffer = Regex.Replace(szCompleteBuffer, "test", "newTest", RegexOptions.IgnoreCase);
//Set the reference so can use same code below...
szCompleteBuffer = newBuffer;
}
//No match, so write out original data
byte[] data = System.Text.UTF8Encoding.UTF8.GetBytes(szCompleteBuffer);
_sink.Write(data, 0, data.Length);
}
else
{
oOutput.Append(szBuffer);
}
}
It is also necessary to add a "using System.Text;" to the top of the class file, and change the original line from:
private long _position;
to:
private long _position;
StringBuilder oOutput = new StringBuilder();
Now modify a page to include some instances of the text "test", and see that they are modified to "newTest" when the page is accessed. Be aware that the example changes instances such as "test1" to "newTest1", it does not change only complete words (although this can be done with more sophisticated regular expressions).
Conclusion
The ability to modify the content created by an ASP.Net page is very powerful. We had a problem where a few pages on a site needed to have the path of <img> references replaced with another value. The problem was that the image tags were in several shared user controls on those pages, so using any other technique to modify the path at runtime would have had a performance impact for all uses of the control. It made more sense to use the above technique on the few pages that required it, rather than modify many user controls and page tags. The technique is limited only by your imagination, as any content created by an ASP.Net page can be modified using this technique.
Send comments or questions to robertb@aspalliance.com.