This chapter tackles a number of advanced topics by digging deeper into the mechanics of how an ASP.NET page is processed. In this first section, you learn how to create a custom BuildProvider
. A BuildProvider
is a .NET class that generates source code from a file automatically. You learn how to create a custom BuildProvider
that builds custom data access components automatically.
Next, you learn how to create a custom ExpressionBuilder
. An ExpressionBuilder
is responsible for parsing an expression into code. For example, when you use the <%$ ConnectionStrings:MyDatabase %>
syntax to refer to a connection string, you are using the ConnectionStringExpressionBuilder
in the background. In this chapter, you learn how to build a custom ExpressionBuilder
that looks up values from an XML file.
You also learn how to work with HTTP Handlers. An HTTP Handler is a .NET class that executes whenever a request is made for a file at a certain path. For example, you can use a custom HTTP Handler to retrieve an image from a database table whenever someone requests a file with the extension .gif
or .jpeg
.
Finally, you learn how to create custom HTTP Modules. An HTTP Module is a .NET class that executes with each and every request. For example, you can implement a custom authentication system by creating a custom HTTP Module. You also can use a custom HTTP Module to create a custom logging module.
When you write an ASP.NET page and save the page to your computer’s file system, the ASP.NET page gets compiled dynamically into a .NET class in the background. The page is compiled dynamically by a BuildProvider
.
The ASP.NET Framework includes a number of BuildProvider
s. Each BuildProvider
is responsible for compiling a file with a particular extension that is located in a particular type of folder. For example, there are BuildProvider
s for Themes, Master Pages, User Controls, and Web Services.
When a BuildProvider
builds, it builds a new class in the Temporary ASP.NET Files folder. Any class added to the folder becomes available to your application automatically. When you use Visual Web Developer, any public properties and methods of the class appear in Intellisense.
You can create your own BuildProvider
s. This can be useful in a variety of different scenarios. For example, imagine that you find yourself building a lot of ASP.NET pages that display forms. You can tediously build each ASP.NET page by hand by adding all the necessary form and validation controls. Alternatively, you can create a new BuildProvider
that takes an XML file and generates the form pages for you automatically.
Or, imagine that you are spending a lot of time building data access components. For example, every time you need to access a database table, you create a new component that exposes properties that correspond to each of the columns in the database table. In this case, it would make sense to create a custom BuildProvider
that generates the data access component automatically.
Let’s start by creating a really simple BuildProvider
. The new BuildProvider
will be named the SimpleBuildProvider
. Whenever you create a file that has the extension .simple
, the SimpleBuilderProvider
builds a new class with the same name as the file in the background. The dynamically compiled class also includes a single method named DoSomething()
that doesn’t actually do anything.
The SimpleBuildProvider
is contained in Listing 27.1.
Example 27.1. App_CodeCustomBuildProvidersSimpleBuildProvider.cs
using System; using System.Web.Compilation; using System.CodeDom; using System.IO; namespace AspNetUnleashed { public class SimpleBuildProvider : BuildProvider { public override void GenerateCode(AssemblyBuilder ab) { string fileName = Path.GetFileNameWithoutExtension(this.VirtualPath); string snippet = "public class " + fileName + @" { public static void DoSomething(){} }"; ab.AddCodeCompileUnit(this, new CodeSnippetCompileUnit(snippet)); } } }
All BuildProvider
s must inherit from the base BuildProvider
class. Typically, you override the BuildProvider
class GenerateCode()
method. This method is responsible for generating the class that gets added to the Temporary ASP.NET Files folder.
An instance of the AssemblyBuilder
class is passed to the GenerateCode()
method. You add the class that you want to create to this AssemblyBuilder
by calling the AssemblyBuilder.AddCodeCompileUnit()
method.
In Listing 27.1, a CodeSnippetCompileUnit
is used to represent the source code for the class. Any code that you represent with the CodeSnippetCompileUnit
is added, verbatim, to the dynamically generated class. This approach is problematic.
Unfortunately, you can use the SimpleBuildProvider
in Listing 27.1 only when building a C# application. It doesn’t work with a Visual Basic .NET application. Because the code represented by the CodeSnippetCompileUnit
is C# code, using the SimpleBuildProvider
with a Visual Basic .NET application would result in compilation errors. The SimpleBuildProvider
would inject C# code into a Visual Basic .NET assembly.
The proper way to write the SimpleBuildProvider
class would be to use the CodeDom
. The CodeDom
enables you to represent .NET code in a language neutral manner. When you represent a block of code with the CodeDom
, the code can be converted to either C# or Visual Basic .NET code automatically. You learn how to use the CodeDom
when we build a more complicated BuildProvider
in the next section. For now, just realize that we are taking a shortcut to keep things simple.
When you add the SimpleBuildProvider
to your project, it is important that you add the file to a separate subfolder in your App_Code folder and you mark the folder as a separate code folder in the web configuration file. For example, in the sample code on the CD that accompanies this book, the SimpleBuildProvider
is located in the App_CodeCustomBuildProviders folder.
You must add a BuildProvider
to a separate subfolder because a BuildProvider
must be compiled into a different assembly than the other code in the App_Code folder. This makes sense because a BuildProvider
is actually responsible for compiling the other code in the App_Code folder.
The web configuration file in Listing 27.2 defines the CustomBuildProviders folder and registers the SimpleBuildProvider
.
Example 27.2. Web.Config
<configuration> <system.web> <compilation> <codeSubDirectories> <add directoryName="CustomBuildProviders"/> </codeSubDirectories> <buildProviders> <add extension=".simple" type="AspNetUnleashed.SimpleBuildProvider" /> </buildProviders> </compilation> </system.web> </configuration>
The web configuration file in Listing 27.2 associates the SimpleBuildProvider
with the file extension .simple
. Whenever you add a file with the .simple
extension to the App_Code folder, the SimpleBuildProvider
automatically compiles a new class based on the file.
Build Providers execute at different times depending on the type of folder. Build Providers associated with the App_Code folder execute immediately after a new file is saved. (Oddly, the Build Provider executes twice.) Build Providers associated with the Web or App_Data folders execute when a file is requested.
For example, adding the file in Listing 27.3 to your App_Code folder causes the SimpleBuildProvider
to create a new class named Mike
.
The actual content of the file that you create doesn’t matter. The SimpleBuildProvider
ignores everything about the file except for the name of the file.
You can see the new file created by the SimpleBuildProvider
by navigating to the Sources_App_Code folder contained in the folder that corresponds to your application in the Temporary ASP.NET Files folder. The contents of the auto-generated file are contained in Listing 27.4.
Example 27.4. mike.simple.72cecc2a.cs
#pragma checksum "C:Chapter27CodeCSApp_CodeMike.simple" "{406ea660-64cf-4c82- b6f0-42d48172a799}" "AD2E00BE337DD88E4E4B07F6B4580617" public class Mike { public static void DoSomething(){} }
Any class added to the Temporary ASP.NET Files folder is available in your application automatically. For example, the page in Listing 27.5 uses the Mike
class.
Example 27.5. ShowSimpleBuildProvider.aspx
<%@ Page Language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> void Page_Load() { Mike.DoSomething(); } </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show SimpleBuildProvider</title> </head> <body> <form id="form1" runat="server"> <div> </div> </form> </body> </html>
The Mike
class appears in Intellisense. For example, if you type Mike
followed by a period, the DoSomething()
method appears (see Figure 27.1).
In the previous section, we created a simple but useless BuildProvider
. In this section, we create a complicated but useful BuildProvider
. The DataBuildProvider
generates a data access component automatically from an XML file. For example, if you add the XML file in Listing 27.6 to your project, then the DataBuildProvider
generates the class in Listing 27.7 automatically.
Example 27.6. App_CodeMovie.data
<Movies> <add name="Title" /> <add name="Director" /> <add name="BoxOfficeTotals" type="Decimal" /> </Movies>
Example 27.7. movie.data.72cecc2a.cs
#pragma checksum "C:Documents and SettingsSteveMy DocumentsASP.NET 3.5 UnleashedChapter27CodeCSApp_CodeMovie.data" "{406ea660-64cf-4c82-b6f0- 42d48172a799}" "2E0F31E6B8F9D4B8687F94F0305E6D15" //——————————————————————————————————————— // <auto-generated> // This code was generated by a tool. // Runtime Version:2.0.50727.1378 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> //——————————————————————————————————————— namespace Data { using System; public partial class Movie { private string _Title; private string _Director; private Decimal _BoxOfficeTotals; public Movie() { } public virtual string Title { get { return this._Title; } set { this._Title = value; } } public virtual string Director { get { return this._Director; } set { this._Director = value; } } public virtual Decimal BoxOfficeTotals { get { return this._BoxOfficeTotals; } set { this._BoxOfficeTotals = value; } } /// <summary>Returns List of Movie</summary> public static System.Collections.Generic.List<Movie> Select(System.Data.SqlClient.SqlConnection con) { System.Collections.Generic.List<Movie> results = new System.Collections.Generic.List<Movie>(); System.Data.SqlClient.SqlCommand cmd = new System.Data.SqlClient. SqlCommand(); cmd.Connection = con; string cmdText = "SELECT Title,Director,BoxOfficeTotals FROM Movies"; cmd.CommandText = cmdText; System.Data.SqlClient.SqlDataReader reader = cmd.ExecuteReader(); int counter; for (counter = 0; reader.Read(); counter = (counter + 1)) { Movie record = new Movie(); record.Title = ((string)(reader["Title"])); record.Director = ((string)(reader["Director"])); record.BoxOfficeTotals = ((Decimal)(reader["BoxOfficeTotals"])); results.Add(record); } return results; } /// <summary>Returns List of Movie</summary> public static System.Collections.Generic.List<Movie> Select(string connectionStringName) { System.Collections.Generic.List<Movie> results = new System.Collections.Generic.List<Movie>(); System.Configuration.ConnectionStringSettings conStringSettings = System.Web.Configuration.WebConfigurationManager.ConnectionStrings [connectionStringName]; string conString = conStringSettings.ConnectionString; System.Data.SqlClient.SqlConnection con = new System.Data.SqlClient.SqlConnection(); con.ConnectionString = conString; try { con.Open(); results = Movie.Select(con); } finally { con.Close(); } return results; } } }
The XML file in Listing 27.6 contains the name of a database table (Movies) and a list of columns from the database table. When you add the file in Listing 27.6 to your project, the class in Listing 27.7 is generated automatically.
The data access component in Listing 27.7 contains a property that corresponds to each of the columns listed in the Movie.data
file. Furthermore, each property has the data type specified in the Movie.data
file.
Notice, furthermore, that the Movie data access component includes two Select()
methods. You can retrieve all the records from the Movies database table in two ways: by passing an open SqlConnection
object to the Select()
method or by passing the name of a connection string defined in the web configuration file to the Select()
method.
The page in Listing 27.8 illustrates how you can use the Movie data access component within an ASP.NET page (see Figure 27.2).
Example 27.8. ShowDataBuildProvider.aspx
<%@ Page Language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> void Page_Load() { grdMovies.DataSource = Data.Movie.Select("Movies"); grdMovies.DataBind(); } </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Untitled Page</title> </head> <body> <form id="form1" runat="server"> <div> <asp:GridView id="grdMovies" Runat="server" /> </div> </form> </body> </html>
Unlike the SimpleBuildProvider
created in the previous section, the DataBuildProvider
uses the CodeDom
to represent code. This means that you can use the DataBuildProvider
in both Visual Basic .NET and C# applications. The DataBuildProvider
generates the data access component in different languages automatically. For example, if you use the DataBuildProvider
in a C# application, the BuildProvider
generates the code in Listing 27.6 in C#.
Unfortunately, the code for the DataBuildProvider
is much too long to include here. The entire code is included on the CD that accompanies the book. The file in Listing 27.9 contains part of the DataBuildProvider
code.
Example 27.9. DataBuildProvider.cs
(Partial)
using System; using System.Collections.Generic; using System.Web.Compilation; using System.CodeDom; using System.Xml; using System.IO; using System.Web.Hosting; namespace AspNetUnleashed { public class DataBuildProvider : BuildProvider { string _className; public override void GenerateCode(AssemblyBuilder ab) { // Load the XML file XmlDocument xmlData = new XmlDocument(); xmlData.Load(HostingEnvironment.MapPath(this.VirtualPath)); // Generate code from XML document CodeCompileUnit dataCode = GetDataCode(xmlData); // Add the code ab.AddCodeCompileUnit(this, dataCode); } private CodeCompileUnit GetDataCode(XmlDocument xmlData) { // Add class _className = Path.GetFileNameWithoutExtension(this.VirtualPath); CodeTypeDeclaration dataType = new CodeTypeDeclaration(_className); dataType.IsPartial = true; // Add constructor AddConstructor(dataType); // Add properties AddProperties(dataType, xmlData); // Add Select method AddSelect(dataType, xmlData); // Add Select with conString overload AddSelectConString(dataType, xmlData); // Create namespace CodeNamespace dataNS = new CodeNamespace("Data"); // Add class to namespace dataNS.Types.Add(dataType); // Create code unit CodeCompileUnit dataCode = new CodeCompileUnit(); // Add namespace to code unit dataCode.Namespaces.Add(dataNS); // Add default namespaces dataNS.Imports.Add(new CodeNamespaceImport("System")); return dataCode; } } }
The DataBuildProvider
’s GenerateCode()
method loads a .data
file into an XmlDocument
. Notice that the VirtualPath
property represents the path of the file that is being built. For example, if you add a file named Products.data
to your project, then the VirtualPath
property would represent the path to the Products.data
file.
Next, the code for the data access component is created from the XML file by the GetDataCode()
method. The GetDataCode()
method makes heavy use of the CodeDom
to generate the code in a language-neutral manner.
Working with the CodeDom
is a strange and tedious experience. You must build up a block of code by building a code tree. In Listing 27.9, a CodeCompileUnit
named dataCode
is created. A CodeNamespace
named dataNS
that represents a namespace is created and added to the CodeCompileUnit
. And, a CodeTypeDeclaration
named datatype
that represents a class is added to the namespace. After the class is created, the methods and properties are added to the class block by block.
An ExpressionBuilder
class generates one expression from another expression. Typically, you use an ExpressionBuilder
to look up a particular value given a particular key.
The ASP.NET Framework includes the following ExpressionBuilder
classes:
AppSettingsExpressionBuilder
—. Retrieves values from the appSettings
section of the web configuration file.
ConnectionStringsExpressionBuilder
—. Retrieves values from the connectionStrings
section of the web configuration file.
ResourceExpressionBuilder
—. Retrieves values from resource files.
The ConnectionStringsExpressionBuilder
has been used throughout this book whenever a connection string has needed to be retrieved.
You use the following syntax when working with an ExpressionBuilder
:
<%$ ConnectionStrings:MyDatabase %>
The <%$
and %>
tags are used to mark an expression that should be parsed by an ExpressionBuilder
. The prefix ConnectionStrings
is mapped to the particular ExpressionBuilder
class that is responsible for parsing the expression.
ExpressionBuilder
s must always be used with control properties. For example, you cannot display a connection string in a page like this:
<%$ ConnectionStrings:MyDatabase %>
Instead, you must display the connection string like this:
<asp:Literal Id="ltlConnectionString" Text='<%$ ConnectionStrings:MyDatabase %>' Runat="server" />
You can create a custom ExpressionBuilder
when none of the existing ExpressionBuilder
classes do what you need. For example, you might want to store your application settings in a custom section of the web configuration file. In that case, you might want to create a custom ExpressionBuilder
that grabs values from the custom configuration section.
In this section, you learn how to extend the ASP.NET Framework by building a custom ExpressionBuilder
class. We’ll create a Lookup ExpressionBuilder
that looks up string values from an XML file.
The LookupExpressionBuilder
class is contained in Listing 27.10.
Example 27.10. App_CodeLookupExpressionBuilder.cs
using System; using System.CodeDom; using System.Web.UI; using System.ComponentModel; using System.Web.Compilation; using System.Xml; using System.Web.Hosting; using System.Web.Caching; namespace AspNetUnleashed { public class LookupExpressionBuilder : ExpressionBuilder { public override CodeExpression GetCodeExpression(BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context) { CodeTypeReferenceExpression refMe = new CodeTypeReferenceExpression(base.GetType()); CodePrimitiveExpression expression = new CodePrimitiveExpression(entry.Expression); return new CodeMethodInvokeExpression(refMe, "GetEvalData", new CodeExpression[] { expression }); } public override object EvaluateExpression(object target, BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context) { return GetEvalData(entry.Expression); } public override bool SupportsEvaluate { get { return true; } } public static string GetEvalData(string expression) { XmlDocument lookupDoc = (XmlDocument)HostingEnvironment.Cache["Lookup"]; if (lookupDoc == null) { lookupDoc = new XmlDocument(); string lookupFileName = HostingEnvironment.MapPath ("~/Lookup.config"); lookupDoc.Load(lookupFileName); CacheDependency fileDepend = new CacheDependency(lookupFileName); HostingEnvironment.Cache.Insert("Lookup", lookupDoc, fileDepend); } string search = String.Format("//add[@key='{0}']", expression); XmlNode match = lookupDoc.SelectSingleNode(search); if (match != null) return match.Attributes["value"].Value; return "[no match]"; } } }
Before you can use the LookupExpressionBuilder
class, you need to register it in the web configuration file. The web configuration file in Listing 27.11 includes an <expressionBuilders>
section that registers the LookupExpressionBuilder
class for the prefix lookup.
Example 27.11. Web.Config
<configuration> <system.web> <compilation> <expressionBuilders> <add expressionPrefix="lookup" type="AspNetUnleashed.LookupExpressionBuilder" /> </expressionBuilders> </compilation> </system.web> </configuration>
The LookupExpressionBuilder
uses an XML file named Lookup.config
to contain a database of lookup values. This file contains key and value pairs. A sample Lookup.config
file is contained in Listing 27.12.
Example 27.12. Lookup.config
<lookup> <add key="WelcomeMessage" value="Welcome to our Web site!" /> <add key="Copyright" value="All content copyrighted by the company." /> </lookup>
Finally, the page in Listing 27.13 uses the LookupExpressionBuilder
. It contains a Literal
control that displays the value of a lookup expression named WelcomeMessage
(see Figure 27.3).
Example 27.13. ShowLookupExpressionBuilder.aspx
<%@ Page Language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show LookupExpressionBuilder</title> </head> <body> <form id="form1" runat="server"> <div> <asp:Literal ID="Literal1" Text="<%$ lookup:WelcomeMessage %>" runat="Server" /> </div> </form> </body> </html>
You create a custom ExpressionBuilder
by inheriting a new class from the base ExpressionBuilder
class. The ExpressionBuilder
class has the following methods:
GetCodeExpression
—. Returns the code that is used to evaluate an expression.
EvaluateExpression
—. Evaluates the expression in the case of no-compile ASP.NET pages.
ParseExpression
—. Returns a parsed version of the expression.
The ExpressionBuilder
class also supports the following property:
SupportsEvaluate
—. When true, the ExpressionBuilder
can be used in no-compile ASP.NET pages.
When you use an ExpressionBuilder
in a normal ASP.NET page, the ExpressionBuilder
returns code that is integrated into the compiled ASP.NET page. The GetCodeExpression()
method returns a block of code that is injected into the compiled ASP.NET page class that gets created in the Temporary ASP.NET Files folder.
Because an ExpressionBuilder
might be used with either a Visual Basic .NET or C# ASP.NET page, the code returned by the GetCodeExpression()
method must be language neutral. This means that you must represent the code that gets executed with the CodeDom
.
In Listing 27.11, the GetCodeExpression()
method returns an instance of the CodeMethodInvokeExpression
class. This class represents an expression that invokes a class method. In this case, the CodeMethodInvokeExpression
class is used to represent the expression LookupExpressionBuilder.GetEvalData()
. In other words, the ExpressionBuilder
adds code to the compiled ASP.NET page class that invokes the GetEvalData()
method contained in Listing 27.10.
As an alternative to creating a normal ASP.NET page, you can create something called a no-compile ASP.NET page. A no-compile ASP.NET page is not compiled dynamically. You create a no-compile ASP.NET page by adding the following attribute to a <%@ Page %>
directive:
<%@ Page CompilationMode="Never" %>
No-compile ASP.NET pages are discussed in Chapter 1, “Overview of the ASP.NET Framework.”
If you want an ExpressionBuilder
to work with no-compile ASP.NET pages, then you must return the value True
from the ExpressionBuilder.SupportsEvaluate
property and implement the EvaluateExpression()
method. The EvaluateExpression
is executed at runtime when the no-compile ASP.NET page is requested. In Listing 27.11, the EvaluateExpression()
method simply calls the GetEvalData()
method.
An HTTP Handler is a .NET class that executes whenever you make a request for a file at a certain path. Each type of resource that you can request from an ASP.NET application has a corresponding handler.
For example, when you request an ASP.NET page, the Page
class executes. The Page
class is actually an HTTP Handler because it implements the IHttpHandler
interface.
Other examples of HTTP Handlers are the TraceHandler
class, which displays application-level trace information when you request the Trace.axd
page and the ForbiddenHandler
class, which displays an Access Forbidden message when you attempt to request source code files from the browser.
You can implement your own HTTP handlers. For example, imagine that you want to store all your images in a database table. However, you want use normal HTML <img>
tags to display images in your web pages. In that case, you can map any file that has a .gif
or .jpeg
extension to a custom image HTTP handler. The image HTTP handler can retrieve images from a database automatically whenever an image request is made.
Or imagine that you want to expose an RSS feed from your website. In that case, you can create a RSS HTTP Handler that displays a list of blog entries or articles hosted on your website.
You can create an HTTP Handler in two ways. You can either create something called a Generic Handler, or you can implement the IHttpHandler
interface in a custom class. This section explores both methods of creating an HTTP Handler.
The easiest way to create a new HTTP Handler is to create a Generic Handler. When you create a Generic Handler, you create a file that ends with the extension .ashx
. Whenever you request the .ashx
file, the Generic Handler executes.
You can think of a Generic Handler as a very lightweight ASP.NET page. A Generic Handler is like an ASP.NET page that contains a single method that renders content to the browser. You can’t add any controls declaratively to a Generic Handler. A Generic Handler also doesn’t support events such as the Page Load
or Page PreRender
events.
In this section, we create a Generic Handler that dynamically generates an image from a string of text. For example, if you pass the string Hello World!
to the handler, the handler returns an image of the text Hello World!
.
The Generic Handler is contained in Listing 27.14.
Example 27.14. ImageTextHandler.ashx
<%@ WebHandler Language="C#" Class="ImageTextHandler" %> using System; using System.Web; using System.Drawing; using System.Drawing.Imaging; public class ImageTextHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { // Get parameters from querystring string text = context.Request.QueryString["text"]; string font = context.Request.QueryString["font"]; string size = context.Request.QueryString["size"]; // Create Font Font fntText = new Font(font, float.Parse(size)); // Calculate image width and height Bitmap bmp = new Bitmap(10, 10); Graphics g = Graphics.FromImage(bmp); SizeF bmpSize = g.MeasureString(text, fntText); int width = (int)Math.Ceiling(bmpSize.Width); int height = (int)Math.Ceiling(bmpSize.Height); bmp = new Bitmap(bmp, width, height); g.Dispose(); // Draw the text g = Graphics.FromImage(bmp); g.Clear(Color.White); g.DrawString(text, fntText, Brushes.Black, new PointF(0, 0)); g.Dispose(); // Save bitmap to output stream bmp.Save(context.Response.OutputStream, ImageFormat.Gif); } public bool IsReusable { get { return true; } } }
The ImageTextHandler
in Listing 27.14 includes one method and one property. The ProcessRequest()
method is responsible for outputting any content that the handler renders to the browser.
In Listing 27.14, the image text, font, and size are retrieved from query string fields. You specify the image that you want to return from the handler by making a request that looks like this:
/ImageTextHandler.ashx?text=Hello&font=Arial&size=30
Next, a bitmap is created with the help of the classes from the System.Drawing name-space. The bitmap is actually created twice. The first one is used to measure the size of the bitmap required for generating an image that contains the text. Next, a new bitmap of the correct size is created, and the text is drawn on the bitmap.
After the bitmap has been created, it is saved to the HttpResponse
object’s OutputStream
so that it can be rendered to the browser.
The handler in Listing 27.14 also includes an IsReusable
property. The IsReusable
property indicates whether the same handler can be reused over multiple requests. You can improve your application’s performance by returning the value True
. Because the handler isn’t maintaining any state information, there is nothing wrong with releasing it back into the pool so that it can be used with a future request.
The page in Listing 27.15 illustrates how you can use the ImageTextHandler.ashx
file. This page contains three HTML <img>
tags that pass different query strings to the handler (see Figure 27.4).
Example 27.15. ShowImageTextHandler.aspx
<%@ Page Language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show ImageTextHandler</title> </head> <body> <form id="form1" runat="server"> <div> <img src="ImageTextHandler.ashx?text=Some Text&font=WebDings&size=42" /> <br /> <img src="ImageTextHandler.ashx?text=Some Text&font=Comic Sans MS&size=42" /> <br /> <img src="ImageTextHandler.ashx?text=Some Text&font=Courier New&size=42" /> </div> </form> </body> </html>
The big disadvantage of a Generic Handler is that you cannot map a Generic Handler to a particular page path. For example, you cannot execute a Generic Handler whenever someone requests a file with the extension .gif
.
If you need more control over when an HTTP Handler executes, then you can create a class that implements the IHttpHandler
interface.
After you create a class that For example, the class in Listing 27.16 represents an Image HTTP Handler. This handler retrieves an image from a database table and renders the image to the browser.
Example 27.16. App_CodeImageHandler.cs
using System; using System.Web; using System.Data; using System.Data.SqlClient; using System.Web.Configuration; namespace AspNetUnleashed { public class ImageHandler : IHttpHandler { const string connectionStringName = "Images"; public void ProcessRequest(HttpContext context) { // Don't buffer response context.Response.Buffer = false; // Get file name string fileName = VirtualPathUtility.GetFileName(context.Request.Path); // Get image from database string conString = WebConfigurationManager.ConnectionStrings [connectionStringName].ConnectionString; SqlConnection con = new SqlConnection(conString); SqlCommand cmd = new SqlCommand("SELECT Image FROM Images WHERE FileName=@FileName", con); cmd.Parameters.AddWithValue("@fileName", fileName); using (con) { con.Open(); SqlDataReader reader = cmd.ExecuteReader(CommandBehavior. SequentialAccess); if (reader.Read()) { int bufferSize = 8040; byte[] chunk = new byte[bufferSize]; long retCount; long startIndex = 0; retCount = reader.GetBytes(0, startIndex, chunk, 0, bufferSize); while (retCount == bufferSize) { context.Response.BinaryWrite(chunk); startIndex += bufferSize; retCount = reader.GetBytes(0, startIndex, chunk, 0, bufferSize); } byte[] actualChunk = new Byte[retCount - 1]; Buffer.BlockCopy(chunk, 0, actualChunk, 0, (int)retCount - 1); context.Response.BinaryWrite(actualChunk); } } } public bool IsReusable { get { return true; } } } }
implements the IHttpHandler
interface, you need to register the class in the web configuration file. The web configuration file in Listing 27.17 includes an httpHandlers
section that associates the .gif
, .jpeg
, and .jpg
extensions with the Image
handler.
Example 27.17. Web.Config
<configuration> <connectionStrings> <add name="Images" connectionString="Data Source=.SQLExpress;Integrated Security=True;AttachDBFileName=|DataDirectory|ImagesDB.mdf; User Instance=True"/> </connectionStrings> <system.web> <httpHandlers> <add path="*.gif" verb="*" type="AspNetUnleashed.ImageHandler" validate="false" /> <add path="*.jpeg" verb="*" type="AspNetUnleashed.ImageHandler" validate="false" /> <add path="*.jpg" verb="*" type="AspNetUnleashed.ImageHandler" validate="false" /> </httpHandlers> </system.web> </configuration>
When you register a handler, you specify the following four attributes:
path
—. Enables you to specify the path associated with the handler. You can use wildcards in the path expression.
verb
—. Enables you to specify the HTTP verbs, such as GET
or POST
, associated with the handler. You can specify multiple verbs in a comma-separated list. You can represent any verb with the * wildcard.
type
—. Enables you to specify the name of the class that implements the handler.
validate
—. Enables you to specify whether the handler is loaded during application startup. When true, the handler is loaded at startup. When false, the handler is not loaded until a request associated with the handler is made. This second option can improve your application’s performance when a handler is never used.
The page in Listing 27.18 uses the ImageHandler
to render its images. The page enables you to upload new images to a database named ImagesDB. The page also displays existing images (see Figure 27.5).
Example 27.18. ImageUpload.aspx
<%@ Page Language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> protected void btnAdd_Click(object sender, EventArgs e) { if (upFile.HasFile) { srcImages.Insert(); } } </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> .fileList li { margin-bottom:5px; } </style> <title>Image Upload</title> </head> <body> <form id="form1" runat="server"> <div> <asp:Label id="lblFile" Text="Image File:" AssociatedControlID="upFile" Runat="server" /> <asp:FileUpload id="upFile" Runat="server" /> <asp:Button id="btnAdd" Text="Add Image" OnClick="btnAdd_Click" Runat="server" /> <hr /> <asp:GridView id="grdImages" DataSourceID="srcImages" AutoGenerateColumns="false" ShowHeader="false" GridLines="None" Runat="server"> <Columns> <asp:ImageField DataImageUrlField="FileName" DataAlternateTextField="FileName" /> </Columns> </asp:GridView> <asp:SqlDataSource id="srcImages" ConnectionString="<%$ ConnectionStrings:Images %>" SelectCommand="SELECT FileName FROM Images" InsertCommand="INSERT Images (FileName,Image) VALUES (@FileName,@FileBytes)" Runat="server"> <InsertParameters> <asp:ControlParameter Name="FileName" ControlID="upFile" PropertyName="FileName" /> <asp:ControlParameter Name="FileBytes" ControlID="upFile" PropertyName="FileBytes" /> </InsertParameters> </asp:SqlDataSource> </div> </form> </body> </html>
The web server included with Visual Web Developer maps all requests to the ASP.NET Framework. For example, if you create an HTTP Handler that handles requests for .gif
files, then you don’t have to do anything special when using the handler with the Visual Web Developer web server.
Internet Information Server, on the other hand, does not map all requests to the ASP.NET Framework. In particular, it does not map requests for .gif
files to ASP.NET. If you want to use a special extension for a handler, then you must configure Internet Information Server to map that extension to the ASP.NET Framework.
If you are serving your pages with Internet Information Server 6.0 (included with Windows Server 2003), then you can create something called a wildcard application mapping. A wildcard application mapping enables you to map all page requests to an application such as the ASP.NET Framework. Follow these steps to configure a wildcard mapping for ASP.NET:
Open Internet Information Services by selecting Start, Control Panel, Administrative Tools, Internet Information Services.
Open the property sheet for a particular website or virtual directory.
Open the Application Configuration dialog box by selecting the Directory tab and clicking the Configuration button.
Select the Mappings tab.
Click the Insert button at the bottom of the Mappings tab to open the Add/Edit Application Extension Mapping dialog box (see Figure 27.6).
In the Executable field, enter the path to the ASP.NET ISAPI DLL. (You can copy and paste this path from the Application Mapping for the .aspx
extension.)
After you complete these steps, all requests made for any type of file are handled by the ASP.NET Framework. If you make a request for a .gif
image, then any handlers that you have registered in the web configuration file for the .gif
extension will execute.
Earlier versions of Internet Information Server, such as the version included with Microsoft Windows XP, do not support wildcard application mappings. You must map each file extension that you want to associate with the ASP.NET Framework one by one. Follow these steps to map the .gif
extension to the ASP.NET Framework:
Open Internet Information Services by selecting Start, Control Panel, Administrative Tools, Internet Information Services.
Open the property sheet for a particular website or virtual directory.
Open the Application Configuration dialog box by selecting the Directory tab and clicking the Configuration button.
Select the Mappings tab (see Figure 27.7).
Click the Add button to open the Add/Edit Application Extension Mapping dialog box.
In the Executable field, enter the path to the ASP.NET ISAPI DLL. (You can copy and paste this path from the Application Mapping for the .aspx
extension.)
In the Extension field, enter .gif
.
After you complete these steps, requests for .gif
images are handled by the ASP.NET Framework. If you have registered an HTTP handler for the .gif
extension in the web configuration file, then the HTTP Handler will execute whenever someone makes a request for a .gif
file.
When you create an HTTP Handler by creating either a Generic Handler or implementing the IHttpHandler
interface, you are creating a synchronous handler. In this section, you learn how to create an asynchronous handler.
The advantage of creating an asynchronous handler is scalability. The ASP.NET Framework maintains a limited pool of threads that are used to service requests. When the ASP.NET Framework receives a request for a file, it assigns a thread to handle the request. If the ASP.NET Framework runs out of threads, the request is queued until a thread becomes available. If too many threads are queued, then the framework rejects the page request with a 503 Server Too Busy response code.
If you execute an HTTP Handler asynchronously, then the current thread is released back into the thread pool so that it can be used to service another page request. While the asynchronous handler is executing, the ASP.NET framework can devote its attention to handling other requests. When the asynchronous handler completes its work, the framework reassigns a thread to the original request and the handler can render content to the browser.
You can configure the ASP.NET thread pool with the httpRuntime
element in the web configuration file. You can modify the appRequestQueueLimit
, minFreeThreads
, and minLocalRequestFreeThreads
attributes to control how many requests the ASP.NET Framework queues before giving up and sending an error.
You create an asynchronous HTTP handler by implementing the IHttpAsyncHandler
interface. This interface derives from the IHttpHandler
interface and adds two additional methods:
BeginProcessRequest
—. Called to start the asynchronous task.
EndProcessRequest
—. Called when the asynchronous task completes.
For example, the file in Listing 27.19 contains an asynchronous handler that grabs an RSS feed from the Microsoft MSDN website.
Example 27.19. App_CodeRSSHandler.cs
using System; using System.Web; using System.Net; using System.IO; namespace AspNetUnleashed { public class RSSHandler : IHttpAsyncHandler { private HttpContext _context; private WebRequest _request; public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) { // Store context _context = context; // Initiate call to RSS feed _request = WebRequest.Create ("http://msdn.microsoft.com/asp.net/rss.xml"); return _request.BeginGetResponse(cb, extraData); } public void EndProcessRequest(IAsyncResult result) { // Get the RSS feed string rss = String.Empty; WebResponse response = _request.EndGetResponse(result); using (response) { StreamReader reader = new StreamReader(response.GetResponseStream()); rss = reader.ReadToEnd(); } _context.Response.Write(rss); } public bool IsReusable { get { return true; } } public void ProcessRequest(HttpContext context) { throw new Exception("The ProcessRequest method is not implemented."); } } }
The handler in Listing 27.19 implements both the BeginProcessRequest()
and EndProcessRequest()
methods required by the IHttpAsyncHandler
interface.
The BeginProcessRequest()
method uses the WebRequest
class to request the page that contains the RSS headlines from the MSDN website. The WebRequest.BeginGetResponse()
method is used to retrieve the remote page asynchronously.
When the BeginGetResponse()
method completes, the handler’s EndProcessRequest()
method is called. This method retrieves the page and renders the contents of the page to the browser.
Before you can use the RSSHandler
, you need to register it in your web configuration file. The web configuration file in Listing 27.20 includes an <httpHandlers>
section that registers the RSSHandler
and associates the handler with the .rss
extension.
Example 27.20. Web.Config
<configuration> <system.web> <httpHandlers> <add path="*.rss" verb="*" type="AspNetUnleashed.RSSHandler"/> </httpHandlers> </system.web> </configuration>
After you register the RSSHandler
, you can execute the handler by making a request for any file that ends with the extension .rss
. If you have a news reader, such as SharpReader
, then you can enter a path like the following in the reader’s address bar:
http://localhost:2026/YourApp/news.rss
The page in Listing 27.21 contains a GridView
and XmlDataSource
control. The XmlDataSource
control calls the RssHandler
to retrieve the headlines that are displayed in the GridView
control (see Figure 27.8).
Example 27.21. ShowRSSHandler.aspx
<%@ Page Language="C#" %> <%@ Import Namespace="System.IO" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> void Page_Load() { string pagePath = Request.Url.OriginalString; string rssPath = Path.ChangeExtension(pagePath, ".rss"); srcRSS.DataFile = rssPath; } </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show RSS Handler</title> </head> <body> <form id="form1" runat="server"> <div> <asp:GridView id="grdRSS" DataSourceID="srcRSS" AutoGenerateColumns="false" Runat="server"> <Columns> <asp:TemplateField HeaderText="Articles"> <ItemTemplate> <asp:HyperLink id="lnkRSS" Text='<%# XPath("title") %>' NavigateUrl='<%# XPath("link") %>' Runat="server" /> </ItemTemplate> </asp:TemplateField> </Columns> </asp:GridView> <asp:XmlDataSource id="srcRSS" XPath="//item" Runat="server" /> </div> </form> </body> </html>
Whenever you request an ASP.NET page, the ASP.NET Framework assigns an instance of the HttpApplication
class to the request. This class performs the following actions in the following order:
Raises the BeginRequest
event.
Raises the AuthenticateRequest
event.
Raises the AuthorizeRequest
event.
Calls the ProcessRequest()
method of the Page
class.
Raises the EndRequest
event.
The entire page execution lifecycle happens during the fourth step. For example, the Page Init
, Load
, and PreRender
events all happen when the Page
class ProcessRequest()
method is called.
The HttpApplication
object is responsible for raising application events. These application events happen both before and after a page is executed.
You might want to handle one of the application events for several reasons. For example, you might want to implement a custom authentication scheme. In that case, you would need to handle the AuthenticateRequest
event to identify the user.
Or you might want to create a custom logging module that tracks the pages that your website users visit. In that case, you might want to handle the BeginRequest
event to record the pages being requested.
If you want to handle HttpApplication
events, there are two ways to do it. You can create a Global.asax
file, or you can create one or more custom HTTP Modules.
By default, the ASP.NET Framework maintains a pool of HttpApplication
objects to service incoming page requests. A separate HttpApplication
instance is assigned to each request.
If you prefer, you can create a custom HttpApplication
class. That way, an instance of your custom class is assigned to each page request.
You can create custom properties in your derived class. These properties can be accessed from any page, control, or component. You also can handle any application events in your custom HttpApplication
class.
You create a custom HttpApplication
class by creating a special file named Global.asax
in the root of your application. Every application can have only one of these files.
For example, the Global.asax
file in Listing 27.22 can be used to track the number of page requests made for any page.
Example 27.22. Global.asax
<%@ Application Language="C#" %> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <%@ Import Namespace="System.Web.Configuration" %> <script runat="server"> private string _conString; private SqlConnection _con; private SqlCommand _cmdSelect; private SqlCommand _cmdInsert; public override void Init() { // initialize connection _conString = WebConfigurationManager.ConnectionStrings["Log"]. ConnectionString; _con = new SqlConnection(_conString); // initialize select command _cmdSelect = new SqlCommand("SELECT COUNT(*) FROM Log WHERE Path=@Path", _con); _cmdSelect.Parameters.Add("@Path", SqlDbType.NVarChar, 500); // initialize insert command _cmdInsert = new SqlCommand("INSERT Log (Path) VALUES (@Path)", _con); _cmdInsert.Parameters.Add("@Path", SqlDbType.NVarChar, 500); } public int NumberOfRequests { get { int result = 0; _cmdSelect.Parameters["@Path"].Value = Request. AppRelativeCurrentExecutionFilePath; try { _con.Open(); result = (int)_cmdSelect.ExecuteScalar(); } finally { _con.Close(); } return result; } } void Application_BeginRequest(object sender, EventArgs e) { // Record new request _cmdInsert.Parameters["@Path"].Value = Request. AppRelativeCurrentExecutionFilePath; try { _con.Open(); _cmdInsert.ExecuteNonQuery(); } finally { _con.Close(); } } </script>
The Global.asax
page in Listing 27.23 handles the Application BeginRequest()
event. You can handle any application event by following the naming pattern Application_
EventName
where EventName
is the name of the HttpApplication
event.
In Listing 27.23, the Application_BeginRequest()
handler is used to record the path of the page being requested. A SqlCommand
object is used to record the page path to a database table named Log.
The Global.asax
file also extends the base HttpApplication
class with a custom property named NumberOfRequests
. This property retrieves the number of requests made for the page at the current path.
Finally, the Global.asax
includes an Init()
method that overrides the base HttpApplication's Init()
method. In Listing 27.23, the Init()
method is used to initialize the SqlConnection
and two SqlCommand
objects used in the Global.asax
file.
The Init()
method is called when the class represented by the Global.asax
is initialized. It is called only once, when the class is first created.
The same instance of the HttpApplication
object is re-used for multiple page requests (although never for multiple page requests at the same time). Any value that you assign to a property in a Global.asax
file is maintained over the multiple page requests.
The page in Listing 27.23 displays the value of the custom property exposed by the Global.asax
file (see Figure 27.9). Notice that the ApplicationInstance
property is used to refer to the instance of the HttpApplication
class associated with the page. Because the Global.asax
file is compiled dynamically in the background, any properties that you declare in the Global.asax
file are exposed as strongly typed properties.
Example 27.23. ShowGlobal.aspx
<%@ Page Language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show Global</title> </head> <body> <form id="form1" runat="server"> <div> This page has been requested <%= this.ApplicationInstance.NumberOfRequests %> times! </div> </form> </body> </html>
An HTTP Module is a .NET class that executes with each and every page request. You can use an HTTP Module to handle any of the HttpApplication
events that you can handle in the Global.asax
file.
Behind the scenes, the ASP.NET Framework uses HTTP Modules to implement many of the standard features of the framework. For example, the ASP.NET Framework uses the FormsAuthenticationModule
to implement Forms authentication and the WindowsAuthenticationModule
to implement Windows authentication.
Session
state is implemented with an HTTP Module named the SessionStateModule
. Page output caching is implemented with an HTTP Module named the OutputCacheModule
, and the Profile
object is implemented with an HTTP Module named the ProfileModule
.
When a new instance of an HttpApplication
class is created, the HttpApplication
loads all of the HTTP Modules configured in the web configuration file. Each HTTP Module subscribes to one or more HttpApplication
events. For example, when the HttpApplication
object raises its AuthenticateRequest
event, the FormsAuthenticationModule
executes its code to authenticate the current user.
In this section, we create a simple authentication HTTP Module. The HTTP Module doesn’t allow you to request a page unless you include the proper query string with the request. The code for the custom HTTP Module is contained in Listing 27.24.
Example 27.24. App_CodeQueryStringAuthenticationModule.cs
using System; using System.Web; namespace AspNetUnleashed { public class QueryStringAuthenticationModule : IHttpModule { public void Init(HttpApplication app) { app.AuthorizeRequest += new EventHandler(AuthorizeRequest); } private void AuthorizeRequest(Object sender, EventArgs e) { // Get context HttpApplication app = (HttpApplication)sender; HttpContext context = app.Context; // If the request is for Login.aspx, exit string path = context.Request.AppRelativeCurrentExecutionFilePath; if (String.Compare(path, "~/login.aspx", true) == 0) return; // Check for password bool authenticated = false; if (context.Request.QueryString["password"] != null) { if (context.Request.QueryString["password"] == "secret") authenticated = true; } // If not authenticated, redirect to login.aspx if (!authenticated) context.Response.Redirect("~/Login.aspx"); } public void Dispose() { } } }
The class in Listing 27.25 implements the IHttpModule
interface. This interface includes two methods:
Init
—. Enables you to subscribe to HttpApplication
events.
Dispose
—. Enables you to clean up any resources used by the HTTP Module.
In Listing 27.25, the Init()
method adds an event handler for the HttpApplication AuthorizeRequest
event. When the HttpApplication
raises the AuthorizeRequest
event, the HTTP Module’s AuthorizeRequest()
method executes.
The AuthorizeRequest()
method checks for a password=secret
query string. If the query string does not exist, then the user is redirected to the Login.aspx
page. (The method also checks whether the user is requesting the Login.aspx
page to avoid a vicious circle.)
Before you can use the QueryStringAuthenticationModule
, you must register the HTTP Module in the web configuration file. The web configuration file in Listing 27.25 includes an <httpModules>
section that registers the module.
Example 27.25. Web.Config
<configuration> <system.web> <httpModules> <add name="QueryStringAuthenticationModule" type="AspNetUnleashed.QueryStringAuthenticationModule"/> </httpModules> </system.web> </configuration>
After you register the HTTP Module, if you attempt to request any page without including the password=secret
query string, then you are redirected to the Login.aspx
page. (If the Login.aspx
page doesn’t exist, you receive a 404 - Not Found
error message.)
In this chapter, you learned how to extend the ASP.NET Framework by extending different parts of the HTTP Runtime. In the first section, you learned how to create a custom BuildProvider
. For example, you learned how to create a BuildProvider
that dynamically generates a data access component from an XML file.
Next, you explored the topic of ExpressionBuilders
. You learned how to use an ExpressionBuilder
to automatically replace one expression with another. For example, we created a custom ExpressionBuilder
that enables you to look up a value from an XML file.
The topic of HTTP Handlers was also explored. You learned two methods of creating custom HTTP Handlers. You learned how to create a Generic Handler, and you learned how to create an HTTP Handler by implementing the IHttpHandler
interface. You also saw how you can improve the scalability of your ASP.NET applications by implementing asynchronous HTTP Handlers.
Finally, you learned two methods of handling applicationwide events. You learned how to create a custom HttpApplication
by creating a Global.asax
file. You also learned how to handle application events by implementing a custom HTTP Module.