Introduction
This article presents a technique for applying watermark images to ASP.NET web forms though the use of an HTTP module. The concept of filtering the HTTP stream as a means for inserting additional markup is presented through the definition of the custom InsertionFilterStream
class. To simplify the creation of modules that would perform this filtering, the base class InsertionModuleBase
is presented with a virtual method FilterString
. Finally, the WatermarkerInsertionModule
class is defined as a functional subclass of the base, providing a practical means for inserting common watermark images across web forms and applications.
Background
In June of 2005, CodeProject member Kenny Young[^] posted a nice article, ASP.NET - C# Application Environment Backsplash[^], in which he demonstrated a technique of using a custom Page
class as a base page for applying watermark images (Kenny refers to them as �backsplashes� in his article). Any page in an ASP.NET application that would display the watermark would inherit from his BasePage
class.
As Kenny demonstrated, a simple style
attribute inserted into the <body>
tag of the output HTML is all that is necessary to create the watermark. I liked the concept of automating a watermark image, particularly to distinguish among development, beta, or production environments, and wondered whether or not such functionality could also be accomplished using an HTTP module. (Readers who are unfamiliar with HTTP modules are encouraged to examine George Shepherd's MSDN article on the topic [^] for an introduction.) Such an implementation would tap into the HTTP stream itself, eliminating the need to derive watermarked pages from a custom Page
class.
At first thought, one might attempt to manipulate the HttpResponse.OutputStream[^] property directly to read from the HTTP stream and insert the required style
attribute. After all, this property holds the Stream
object that represents the outgoing HTTP content, and the HttpResponse
object is certainly available in the context of an HTTP module. Unfortunately, this possibility is prevented by the fact that the OutputStream
property actually contains an HttpResponseStream
type, which is write-only. Attempts to read from an HttpResponseStream
object result in an exception being thrown.
Filtering the HTTP Stream
What is allowed is the filtering of the output stream. The HttpResponse
object exposes a Filter[^] property, which itself is a Stream
type. If Filter
is set, then any output written to the response stream is redirected to the filtering stream. The filter may perform its manipulation of the contents, but must then write its results back to the original stream. This effectively forms a chain of two streams; indeed, multiple filtering streams may be applied, provided each assumes the responsibility of writing back to the stream object it is replacing as the Filter
.
Our filtering stream is defined by the InsertionFilterStream
class included with the source download, the constructor of which follows:
public class InsertionFilterStream : Stream
{
private Stream _originalStream;
private Encoding _encoding;
private FilterReplacementDelegate _replacementFunction;
. . .
public InsertionFilterStream(Stream originalStream
, FilterReplacementDelegate replacementFunction
, Encoding encoding)
{
_originalStream = originalStream;
_replacementFunction = replacementFunction;
_encoding = encoding;
}
. . .
}
Three important pieces of information will be passed in to the constructor for our filtering object. The first is the original Stream
object for which we�re performing our string manipulations. This will likely be the HTTP output stream, but there could be one or more additional filter streams already in place. In practice, we�ll use whatever stream is already set for the HttpResponse.Filter
property; if no other filter were already applied, this would be the output stream itself.
The second piece of information is a delegate, pointing to the function that will perform a string replacement. The delegate is defined with the following signature:
public delegate string FilterReplacementDelegate(string s);
As we�ll see later, this offers simplicity for the developer; rather than dealing with encoded byte arrays, the developer will use common string
objects.
The final parameter for the constructor is an Encoding
object. This will also be retrieved from the HttpResponse
object through its ContentEncoding
property, allowing our implementation to be neutral to the given character set. These three objects are stored in private
class variables for later use.
The most important method in our filtering Stream
subclass is the Write
method. When Write
is called, it is passed a byte array, already encoded according to the response stream�s character set. Our overridden method performs these important steps: it converts the encoded byte array to a plain string
for ease of use, passes that string to the replacement function delegate, re-encodes the result as a byte array, and writes the bytes back to the original output stream.
public override void Write(byte[] buffer, int offset, int count)
{
string sBuffer = _encoding.GetString(buffer, offset, count);
string sReplacement = _replacementFunction(sBuffer);
_originalStream.Write(_encoding.GetBytes(sReplacement)
, 0, _encoding.GetByteCount(sReplacement));
}
The Base HTTP Module
With our InsertionFilterStream
class prepared to intercept writes to the output stream, we�ll use an HTTP module to hook into the web application�s events and instate our filter. An HTTP module is created by implementing the IHttpModule[^] interface, and installed in the HTTP pipeline by adding an item to the <httpModules>
subsection of the <system.web>
section of a web.config or machine.config file.
The IHttpModule
contract requires the implementer to define two methods: Init
and Dispose
. The ASP.NET runtime will call Init
for each installed module, passing each the HttpApplication
object. With this object, the module may install handlers for any number of events exposed by the web application. In our case, we only need to hook into the BeginRequest
event.
To support a simple model for creating modules that rely on string replacement functions to insert additional markup, we�ll define a base class that handles the appropriate event interaction with the web application. We�ll also include a virtual method called FilterString
which is sent to the InsertionFilterStream
object as a delegate. The InsertionModuleBase
class is shown below in its entirety:
public class InsertionModuleBase : IHttpModule
{
private FilterReplacementDelegate _replacementDelegate = null;
private InsertionFilterStream _filterStream = null;
public InsertionModuleBase()
{
}
public void Dispose()
{
}
public void Init(HttpApplication app)
{
app.BeginRequest
+= (new EventHandler(this.Application_BeginRequest));
}
private void Application_BeginRequest(object source, EventArgs e)
{
HttpApplication app = source as HttpApplication;
if (app != null)
{
_replacementDelegate = new FilterReplacementDelegate(FilterString);
_filterStream = new InsertionFilterStream(
app.Response.Filter
, _replacementDelegate
, app.Response.ContentEncoding
);
app.Response.Filter = _filterStream;
}
}
protected virtual string FilterString(string s)
{
return s;
}
}
A Simple Insertion Module
An insertion module may now be created by subclassing InsertionModuleBase
and overriding the FilterString
method. Here is a simple but complete example that replaces the <body>
tag of the HTML output with one adorned with a background color, and a heading <table>
displaying the text �Development�.
public class SimpleInsertionModule : InsertionModuleBase
{
protected override string FilterString(string s)
{
return s.Replace("<body>"
, "<body bgcolor='#EFEFEF'>"
+ " <table width='100%' bgcolor='pink'>"
+ " <tr><td>Development</td></tr>"
+ " </table>");
}
}
Provided a compiled version of this class (and the supporting classes InsertionModuleBase
and InsertionFilterStream
) were available to a web application (i.e. installed in the global assembly cache, or copied into the application�s /bin directory), a simple addition to the application�s web.config file is all that is necessary for this string replacement to occur with all the application�s web forms. Alternatively, the addition may be made to the server�s machine.config file, in which case all applications on the server would have the functionality available.
Custom HTTP modules are included in the HTTP pipeline by adding items to the <httpModules>[^] section of web.config (which is nested within <system.web>
). The following format is used to add a module to the HTTP pipeline:
<add type="classname,assemblyname" name="modulename"/>
So to indicate the inclusion of the SimpleInsertionModule
defined above, assuming the namespace MyNamespace
, compiled into a .dll called InsertionModules.dll, we could use the following fragment in web.config or machine.config:
<configuration>
<system.web>
<httpModules>
<add type=�MyNamespace.SimpleInsertionModule,InsertionModules�
Name=�SimpleInsertionModule� />
</httpModules>
</system.web>
</configuration>
The WatermarkerInsertionModule Class
To accomplish the original challenge, the inclusion of a watermark image using an HTTP module rather than a Page
subclass, we again subclass InsertionModuleBase
and override the FilterString
method. In addition to watermarking, we�ll provide the ability to add a header and/or footer. We�ll also use regular expressions for more robust pattern matching and case-insensitive replacements. The WatermarkerInsertionModule
class is shown below in its entirety:
public class WatermarkerInsertionModule : InsertionModuleBase
{
protected override string FilterString(string s)
{
string sReturn = s;
string sImage
= ConfigurationSettings.AppSettings["WatermarkerInsertionModule_Image"];
string sTopContents
= ConfigurationSettings.AppSettings["WatermarkerInsertionModule_Top"];
string sBottomContents
= ConfigurationSettings.AppSettings["WatermarkerInsertionModule_Bottom"];
if (sImage != String.Empty && sImage != null)
{
Regex rHeadEnd = new Regex("(</head>)", RegexOptions.IgnoreCase);
string sStyle = "<style>body {"
+ "background-image: url(" + sImage + "); "
+ "background-attachment: scroll; "
+ "background-repeat: repeat; "
+ "background-color: transparent; "
+ "}</style>";
sReturn = rHeadEnd.Replace(sReturn, sStyle + "</head>");
}
if (sTopContents != String.Empty && sTopContents != null)
{
Regex rBodyBegin = new Regex("<body.*?>", RegexOptions.IgnoreCase);
Match m = rBodyBegin.Match(sReturn);
if (m.Success)
{
string matched = m.Groups[0].Value;
sReturn = sReturn.Replace(matched, matched + sTopContents);
}
}
if (sBottomContents != String.Empty && sBottomContents != null)
{
Regex rBodyEnd = new Regex("</body>", RegexOptions.IgnoreCase);
sReturn = rBodyEnd.Replace(sReturn, sBottomContents + "</body>");
}
return sReturn;
}
}
As in the previous SimpleInsertionModule
example, the deployment of WatermarkerInsertionModule
involves the following steps:
- Install the compiled .dll containing the
WatermarkerInsertionModule
, InsertionFilterStream
, and InsertionModuleBase
classes to either the global assembly cache, or the web application�s /bin directory.
- Modify either the machine.config file on the server, or the web.config file in the web application directory, to include the appropriate
<add>
item in the <httpModules>
section of <system.web>
.
Additional Considerations
While boiling down this replacement process to a single overridden FilterString
method in a custom subclass may provide a convenience to the developer, it�s not without a price. Converting the byte array to a string and back again entails more overhead than just manipulating the byte array directly. For that matter, using an HTTP module is its own form of overhead. While it may be appropriate to watermark using this technique for lower-volume development or beta deployments, it may not be the best option for, say, applying a common copyright notice in footers of pages on a high-volume production site. The developer must make the decision as to whether the performance hit from this technique is worth the functionality for his or her own environment.
Additionally, this technique assumes that the server is buffering page output, so the complete page may be sent to the client in one call to Write
(this is the default ASP.NET behavior). If page buffering is disabled (by setting Page.Response.BufferOutput
to false
), this technique may produce unexpected results.
It is also important to note that when configured in <httpModules>
, the module receives all requests passed to it by the ASP.NET runtime. There may be situations, for example, an .aspx page that changes the content-type and outputs a binary stream, or web methods that return SOAP-formatted messages, in which you would want the module to be disabled, or at least to ignore the request. You may wish to modify the module source to check for the content type, or choose to place such pages in their own subdirectory, incorporating additional web.config files with <remove>
tags in the <httpModules>
section to disable the module entirely under those types of circumstances.
About the Demo Project
The demo project contains the file InsertionModuleBase.dll, which is a compiled version of all the custom classes mentioned in this article. Web.config files are used in both the main project directory and in subdirectories to configure the inclusion or exclusion of the WatermarkerInsertionModule
for different page options.
Summary
Presented as an alternative to creating a base Page
class, the WatermarkerInsertionModule
provides an implementation of the IHttpModule
interface for the purpose of applying watermark images and common headers and footers to web forms. It inherits from the class InsertionModuleBase
in which is defined the consumption of the web application�s BeginRequest
event, and the incorporation of a custom filter for the HTTP output stream. The InsertionFilterStream
intercepts calls to the Write
method, and applies the string replacement function defined in WatermarkerInsertionModule
. Provided a developer�s environment allows for page buffering and the requisite performance hit that comes with string conversion and module overhead, the WatermarkerInsertionModule
can be a convenient utility for performing automated watermarking on web forms.