Topics in This Chapter
HTTP Request and Response Objects: These ASP.NET classes correspond closely to the HTTP request and response specifications, and expose information about a request or response through class properties.
Configuration Files: Configuration files provide an easy way to specify the behavior and performance of a Web application. Focus is on the web.config
file.
ASP.NET Security: Forms Authentication is a platform-neutral technique that can be used to control who can access a Web page. An example demonstrates the use of Forms Authentication to manage authentication and authorization.
State Management: Both the Application
and Session
classes can be used to maintain information during a Web session or during the life of a Web application.
Caching: Data caching and output caching can be used to improve Web application performance.
Accessing Web Resources: The WebRequest
and WebResponse
classes provide a simple way to request and process Web pages for the purpose of extracting page content.
HTTP Pipeline: The HTTP pipeline is the combination of events, HTTP modules, and HTTP handlers that affect how requests and responses are handled as they pass between a client and server.
Chapter 16, “ASP.NET Web Forms and Controls,” dealt with the most visible aspect of ASP.NET—the Web page. It described a Web application in terms of the controls, HTML, and compilable C# code that comprise it. Much of the emphasis was on how to construct the interface presented to a client. This chapter shifts the focus to the underlying details of ASP.NET support for the Hypertext Transfer Protocol (HTTP), which defines how Web requests and responses are transported between client and host.
You'll find that a discussion of the ASP.NET environment is heavily tilted toward those issues that fall under the responsibility of the Web server—as opposed to a Web client. This is only natural because one of the purposes of ASP.NET is to enable developers and architects to manage the difficult side of Web performance, while allowing Web clients to remain blissfully thin and unaffected.
The chapter begins with a little background information on the structure of a response and request—as defined by the HTTP standard. It shows how the HttpRequest
and HttpResponse
objects serve as proxies that expose and extend the information you'll find in the standard HTTP message structure. Next, the role of configuration files is discussed—a significant role that allows XML elements to be used to manage the behavior and performance of Web applications at an application, domain, and machine level. Controlling who can gain access to a Web page or resource is the next topic. Included is an overview of the .NET options for user authentication and authorization, as well as a detailed example of forms authentication.
One of the challenges of designing a Web application is determining how to maintain and manage state information. A discussion of how to persist data between Web requests includes examples of using both Session
and Application
objects to store information. ASP.NET also offers data and output caching as a way to store state information or to buffer frequently used data or Web pages. When used correctly, caching can greatly improve Web performance. Its advantages and disadvantages are considered.
The client side is not totally abandoned. The penultimate section demonstrates how to access Web resources using the WebRequest
and WebResponse
classes. An example uses an HttpWebRequest
object to retrieve a Web page and glean information about the server.
The final section discusses the HTTP pipeline—a metaphor for the series of internal events that occur along the roundtrip journey that a request and response travel.
HTTP specifications[1] concisely define messages as “requests from client to server and responses from server to client.” At its most elemental level, the role of ASP.NET is to enable server-based applications to handle HTTP messages. The primary way it does this is through HTTPRequest
and HttpResponse
classes. The request class contains the HTTP values sent by a client during a Web request; the response class contains the values returned to the client.
An HttpRequest
object is available to a Web application via the Page.Request
or Context.Request
property. The object's properties represent the way that .NET chooses to expose the content to an underlying HTTP request message. Consequently, the best way to understand HttpRequest
is to examine the layout of the message it represents.
Figure 17-1 represents the general structure of the HTTP request message as defined by the HTTP/1.1 specifications.
Unless you are writing a browser, it is not necessary to understand the full details of the standard. However, a general understanding is useful to a developer who needs to extract information from the HttpRequest
object. A few observations:
The Request-Line
consists of three parts: the method token, the Request-URI, and the protocol version. Several methods are available, the most common being the POST
and GET
methods described in Chapter 16. The Uniform Resource Identifier (URI) specifies the resource being requested. This most commonly takes the form of a Uniform Resource Locator (URL), but can be a file or other resource. The protocol version closes out the Request-Line
.
The general-header
is used to pass fields that apply to both requests and responses. The most important of these is the cache-control
field that specifies, among other things, what can be cached and how long it can be cached.
The request-header
is used by the client to pass additional information about the request, as well as the client. Most of the HttpRequest
object's properties correspond to values in this header.
For debugging—or simply out of curiosity—you may want to view the raw contents of the HTTP request. A simple way to do this is to use the SaveAs
method of the HttpRequest
class to write the request to a file where it can be viewed with a text editor. The method, as shown here, takes two parameters: the name of the output file and a bool
type switch that indicates whether HTTP headers are to be included in the output.
this.Request.SaveAs("c:\myrequest.txt",true);
Posting a form containing two text boxes to the Web server generates this sample output. Of most interest are the browser description, referring Web page, and text box content values.
POST /ideas/panel.aspx HTTP/1.1 Cache-Control: no-cache Connection: Keep-Alive Content-Length: 158 Content-Type: application/x-www-form-urlencoded Accept: image/gif, image/x-xbitmap, image/jpeg, */* Accept-Encoding: gzip, deflate Accept-Language: en-us Cookie: ASP.NET_SessionId=uszrfs45z4f20y45v0wyyp45 Host: localhost Referer: http://localhost/ideas/panel.aspx User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.1.4322; .NET CLR 2.0.40607) __VIEWSTATE=%2FwEPDwULLTEwODExMTgwOTAPZBYCA ... &txtFirstName=joanna&txtLastName=larson&btnSubmit=Submit
Table 17-1 summarizes selected properties of the HttpRequest
class. Where applicable, it includes the underlying header and message field on which its content is based.
Table 17-1. HttpRequest
Properties
HttpRequest Property | Request Message Field | Description |
---|---|---|
| — | String array of client-supported MIME accept types. |
| (request-header) User-Agent | Identifies software program making request. |
| — | Returns the client's security certificate as an |
| (entity-header) Content-Type | Character set of the entity body. |
(entity-header) Content-Length | Size of client request. | |
| (entity-header) Content-Type | MIME content type of request. |
| Not defined in HTTP protocol | Collection of client's cookie variables. |
| Request-Line URI | The virtual path of the currently requested page. |
| — | Files uploaded by the client. |
| Body | The collection of form variables including |
| general and request-header fields | Collection containing content of headers contained in request message. |
| Request-Line method | HTTP data transfer method. |
| (request-header) Authorization | Has user been authenticated. |
| Request-Line | True if HTTPS used. |
| Request-Line URI | Virtual path of current request. |
| — | Physical path of current page. |
| Request-Line URI | Query string arguments. |
| Request-Line URI | Part of a URL following the domain name. |
| Request-Line method field | HTTP data transfer method ( |
| (entity-header) Content-Length | Number of bytes in input stream. |
| Request-Line URI field and Host field of header | URL of current request. |
(request-header) Referer field | Information about the URL of the client's previous request that linked to the current URL. | |
| (request-header) User-Agent | String containing raw information about the client software used for request—usually a browser. |
| — | IP host address of the remote client. |
The built-in features of ASP.NET reduce the amount of direct interaction required between an application and the HttpRequest
object. For example, we saw in Section 16.2, “Web Forms Controls,” that ASP.NET Web controls generate code that is automatically tailored to the client's browser—eliminating the need for the application code to implement logic to identify the browser. There are, however, some cases where an application must access the object fields directly. Logging Web statistics is one; another is working with cookies.
Cookies are used to store information on the computer of a client that has cookies enabled on his browser. The browser is responsible for passing the cookie value as part of the request and handling any cookies returned from the server. The Cookies
attribute references the collection of cookies returned by the browser. The following code loops through the collection displaying the name, value, and expiration date of cookies in the collection.
// Read cookies returned by browser
foreach(string cookieName in Request.Cookies.AllKeys){
HttpCookie cookie = Request.Cookies[cookieName];
Response.Write(cookie.Name+" = "+cookie.Value
+"<br>Expires: "+cookie.Expires);
}
The only cookie in this collection is the ID used by ASP.NET to identify sessions. ASP.NET's use of cookies is discussed further in the next section.
.NET_SessionId = avsqkn5501m3u2a41e3o4z55 Expires: 1/1/0001 12:00:00 AM
The HttpResponse
class contains properties that encapsulate the information returned in a response message. It also provides methods that construct the response and send it to the requestor. As we did with the Request
object, let's begin by taking a quick look at the underlying HTTP response message represented by the Response
object (see Figure 17-2).
The server responds with a status line that includes the message's protocol version, a success or error code, and a textual description of the error code. This is followed by the general header and the response header that provides information about the server. The entity header provides metainformation about the body contents or the resource requested.
ASP.NET provides the HttpWebRequest
and HttpWebResponse
classes for working with HTTP requests and responses, respectively. They are discussed in detail later in the chapter, but let's take a preliminary look at how they can be used to display portions of a response message.
Listing 17-1 contains a simple application that sends a request to a user-entered URL, receives the response, and extracts the status code and server description. Run the program from the command line by typing in the program name and domain name you want to contact:
Example: |
|
Output: |
|
Example 17-1. Getting Status and Server Information from a Response Message
//File: showserver.cs using System; using System.Net; class WebClient { // To run, type in: showserver <domain name> public static void Main(string[] args) { HttpWebRequest request; HttpWebResponse response; if(args.Length>0) { string url="http://"+args[0]; // Create a request to the URL request = (HttpWebRequest) WebRequest.Create(url); try { response = (HttpWebResponse) request.GetResponse(); Console.WriteLine("Web Host: "+response.Server); Console.WriteLine("Response Status: "+ response.StatusCode); } catch ( Exception ex) { Console.Write(ex.Message); } } else { Console.Write("You must enter a domain name."); } } }
Table 17-2 lists selected properties of the HttpResponse
class. Some of those excluded exist only for ASP compatibility and have been deprecated by newer properties.
Table 17-2. Selected HttpResponse
Properties
Property | Description |
---|---|
| Set to |
|
Example: |
| Gets or sets the character set of the output stream. |
| An |
| String value containing MIME type of the output—for example, “text/html”. |
| Collection of cookies sent to the client. |
| A developer-written |
| Indicates whether client is still connected to the server. |
|
|
| Status code returned in the response message status line. |
| Status code description returned in the response message status line. |
Particularly noteworthy is the Cache
property, which can greatly affect how pages are displayed to a browser. It is used to define a caching policy that dictates if and how long pages are cached. We'll look at this property in Section 17.5, “Caching.”
An example that displayed the contents of a cookie was presented in the discussion of the HttpRequest
class. Let's extend that example by demonstrating how the response object is used to create and return a cookie to the client.
// Create a cookie
HttpCookie myCookie = new HttpCookie("userid","007");
// Cookie will live for 10 minutes
// Timespan(days, hours, minutes, seconds)
myCookie.Expires = DateTime.Now.Add(new TimeSpan(0,0,10,0));
// Add to collection
Response.Cookies.Add(myCookie);
// Later... Read specific cookie
myCookie = Request.Cookies["userid"];
if(myCookie !=null) Response.Write("userid: "+myCookie.Value);
The Response.Write
method, which we have used in numerous examples to emit output into the response stream, is the most commonly used method. Other useful methods include the following:
Redirect
. Redirects the client's request to another URL.
AppendHeader
. Adds an HTTP header to the response stream.
ClearContent
. Clears the contents of the response stream.
End
. Stops page execution and sends buffered output to the client.
WriteFile
. Places the contents of a file directly into the output stream.
A simple, but useful, example (see Listing 17-2) illustrates how these methods can be used to download a file. The client request for this page includes a query string with the name of a requested file. The page locates the file (or returns an error message) and uses WriteFile
to send it to the user. The AppendHeader
, ClearContent
, and End
methods prepare and manage the response stream.
Example 17-2. Using HttpResponse to Download a File
//File: requestfile.aspx <%@ Page Language="C#" %> <script Language="C#" runat="Server"> private void Page_Load(object sender, EventArgs e) { //http://localserver/ideas/requestfile.aspx?file=notes.txt string fileRequest = Request.QueryString["file"]; if(fileRequest!=null) { // File is store in directory of application string path = Server.MapPath(fileRequest); System.IO.FileInfo fi = new System.IO.FileInfo(path); if (fi.Exists) { Response.ClearContent(); // Clear the response stream // Add a header to indicate attachment type Response.AppendHeader("Content-Disposition", "attachment;filename="+fi.Name); Response.AppendHeader("Content-Length", fi.Length.ToString()); // Use octet-stream to indicate unknown media type Response.ContentType="application/octet-stream"; // Write file to output stream Response.WriteFile(fi.FullName); Response.End(); // Flush buffer output to the client } else { Response.Write(path+" does not exist."); } } else { Response.Write("No Download file was specified."); } } </script>
One of the advantages of working with ASP.NET, as a developer or administrator, is the ease of configuring and customizing the settings. The use of XML-based configuration files replaces the previous ASP approach that required shutting down and restarting server software to change a setting or replace DLLs. Now, changes that affect the underlying behavior of a Web page can be made dynamically. The beauty of this approach is that changes can be made using a simple text editor—although an XML editor is preferable—and that ASP.NET automatically detects and applies these changes when they occur.
Configuration information is stored in a required machine.config
file and optional web.config
files. The format of these files is the same; the difference is their scope.
As shown in Figure 17-3, configuration files can be placed in a hierarchy on the Web server. Each file lower down in the hierarchy overrides the settings (with a few exceptions) of files above it. The machine.config
file is at the top of the hierarchy, and its settings apply to all applications running on the machine. Web.config
files placed in application directories and subdirectories apply their settings to a specific application or pages in an application.
As an alternative to a text editor, ASP.NET includes a configuration editor with the IIS management console. It is accessed from the Properties menu of a virtual directory.
The web.config
file is an XML-formatted text file comprising numerous sections and subsections. Listing 17-3 offers a sampling of sections you're likely to find most useful: <configSections>
, <appSettings>
, <location>
, <system.web>
, and <connectionStrings>
. After summarizing key features of these predefined sections, we'll look at how to create a custom configuration section.
Example 17-3. Sample web.config
File (continued)
<configuration> <!-- (1) Define custom configurations --> <configSections> <section name="RewriterConfig" type="RewriteSectionHandler, URLRewriter" /> </configSections> <RewriterConfig> <!-- Contents of custom configuration section --> </RewriterConfig> <!-- (2) Place application data in here --> <appSettings> <add key="mainFont" value="arial" /> <add key="fontSize" value="2" /> </appSettings> <!-- (3) Define system.web settings for a directory --> <location path="calculatorsmi"> <system.web> <trace enabled="true" pageOutput="true" /> </system.web> </location> <!-- (4) Main ASP.NET Application settings --> <system.web> <sessionState cookieless="false" timeout=20 /> ... </system.web> <!-- (5) Connection string for database --> <connectionStrings> <!-- connection string description --> </connectionStrings> </configuration>
This area is used by developers to hold constant data values required by an application. These values typically include preference settings that define the appearance of a Web page. This entry illustrates how font settings are added using an <add>
element and a key-value pair of attributes:
<add key="mainFont" value="arial" />
Values are retrieved from the appSettings
section using the static AppSettings
property of the ConfigurationSettings
class:
myFont = ConfigurationSettings.AppSettings["mainFont"];
As mentioned previously, web.config
files can be placed hierarchically within a directory structure, with files at lower levels overriding the settings of files in parent directories. This approach permits you to tailor the actions of ASP.NET down to the application and page level. However, this flexibility brings with it the potential problem of keeping track of the settings in multiple configuration files. As an alternative, ASP.NET provides a way to configure the settings for multiple directories in a single web.config
file by using <location>
elements. The <location>
section operates as a configuration file within a configuration file. The element has a path
attribute that takes the value of a virtual directory name. The settings within the contained <system.web>
block apply to the virtual directory.
As an example, suppose that during application development we want to view the trace information as we test our applications. Because we don't want this voluminous output in the final product, we can set up a virtual “test” directory and enable tracing for that directory only.
<location path="test"> <system.web> <trace enabled="true" pageOutput="true" /> </system.web> </location>
This has the same effect as placing a web.config
file with this trace setting in the physical directory associated with the “test” virtual directory.
This is the area in web.config
where an administrator can configure just about any aspect of the ASP.NET environment. It can be used to set session time-out length, user authentication and authorization rules, the type of session state management used, and the default culture used for processing requests. Table 17-3 summarizes the more important elements in this section.
Table 17-3. Selected system.web
Elements
Element | Use |
---|---|
| Sets default language and debug option. |
| Defines custom errors and error pages. |
| Enables or disables the trace feature for an application. |
| Sets default Web page attributes. |
| Defines response/request encoding and culture-specific setting for Web pages. |
| Used to configure process setting for an IIS Web server. |
| Specifies authentication mode for a client: |
| Allows or denies access to resources. |
| Defines how the Membership class manages user credentials and authentication. |
| Specifies whether client impersonation is used on a Web page request. |
| Configures the session state features of ASP.NET. |
Of particular interest are the authentication
, authorization
, and membership
elements that are discussed in Section 17.3, “ASP.NET Application Security.” Other useful sections are summarized here.
Use the debug
attribute to indicate whether debugging symbols will be included in the compiled code. The defaultLanguage
attribute sets the default language used in a Web page's script blocks. Multiple languages can be selected by using a semicolon to separate them.
<compilation debug="true" defaultLanguage="C#" />
The primary use of this section is to redirect errors to custom Web pages. Here is an entry that establishes a default error page, and a page to handle the HTTP 404 status code that arises when a resource cannot be found:
<customErrors defaultRedirect = "defaultError.aspx" mode="On" > <error statusCode="404" redirect="Error404.aspx" /> </customErrors>
The mode
attribute takes the value On
, Off
, or RemoteOnly
. The latter value, which is also the default, specifies that the default page is called only if the request
comes from a machine other than the Web server. This permits local developers to see details of the actual error, whereas clients see only the custom page. Off
should be used when a developer is testing pages on a remote machine. Note that multiple <error>
elements can be included in the section.
The <trace>
element is used to enable application-level trace logging. This is in contrast to the trace
attribute of the <%@Page>
directive that provides only page-level tracing. Here is an example that illustrates the use of the <trace>
element and its associated attributes:
<trace enabled="true" pageOutput="false" traceMode="SortByTime" requestLimit="5", localOnly="false" </trace>
Aside from the enabled attribute that turns tracing on or off, the most interesting attributes are pageOutput
and localOnly
.
pageOutput
specifies whether the trace log is appended to the Web page for display on the client's browser. If set to false
, the output can be viewed in the trace.axd
page of the application's root. Here is an example:
Application URL: |
|
Trace log: |
|
The requestLimit
attribute sets the maximum number of trace logs maintained.
localOnly
indicates whether tracing is enabled for localhost
users only or all users. The default is true
.
One of the more powerful features of ASP.NET is the ability to maintain state information for a session; and the <sessionState>
element plays a definitive role in selecting how it is implemented. By setting its mode
attribute to one of four values, the software architect can specify where the session state information is stored.
off
disables the use of session state management.
Inproc
enables in-process state management (aspnet_state.exe
).
StateServer
state is stored by a surrogate process running on a selected server.
SqlServer
state is maintained in a temporary SQL Server table.
These options are described in detail in Section 17.4. Our interest in this section is to understand the elements and attributes that define how ASP.NET manages session state. To illustrate, here are the default settings for the <sessionState>
element:
<sessionState mode="InProc" stateConnectString="tcpip=127.0.0.1:42424" sqlConnectionString="data source=127.0.0.1;Trusted_Connection=yes" cookieless="false" timeout="15" />
The timeout
attribute specifies the number of minutes the session can be idle before it is abandoned. The cookieless
attribute indicates whether the identification of the client session is maintained using a cookie or by mangling the URL (placing encoded session information in the URL line). The other two attributes are dependent upon the mode
setting: stateConnectString
specifies the IP address of the machine running the Session State Server process, and sqlConnectionString
contains the connection string value used to access SQL Server if it is used to maintain state information.
Prior to .NET version 2.0, developers usually resorted to storing connection strings as a key-value pair in the <appSettings>
section. ASP.NET 2.0 now provides a predefined <connectionStrings>
section for this purpose. As shown in this example, the section takes three attributes: name
, connectionString
, and providerName
.
<connectionStrings> <add name="movies" connectionString= "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=/movies.mdb;" providerName="System.Data.OleDb"/> </connectionStrings>
The connection string is retrieved using the ConfigurationSettings
class, which retrieves the string associated with a specified name
attribute. Note that multiple strings may be defined.
string cs= ConfigurationSettings.ConnectionStrings["movies"].ConnectionString;
The web.config
file can be a convenient place to stash information needed by an application. We saw earlier how simple name/value pairs can be stored in the <appSettings>
section. Suppose, however, your application requires data represented by a more complex XML structure. The solution is to add a custom configuration section to web.config
. However, it is not as simple as choosing a configuration name and inserting appropriately formatted code into the configuration file. When you try to access a Web application that uses the configuration file, you'll receive an Unrecognized Configuration Section error.
It turns out that each section in web.config
has a special handler that parses the section and makes its content available to an application. This handler is a class that implements the System.Configuration.IConfigurationSectionHandler
interface and is declared in the <configSections>
section of the web.config
file.
You can view the section handlers for the standard predefined sections, such as <appSections>
in the machine.config
file.
To demonstrate how section handlers work, let's create one for the <RewriterConfig>
section shown in the code that follows. The first thing to note is the <configSections>
element that contains a definition of the new section. Its type attribute specifies the handler (class) that parses the <RewriterConfig>
section. The data section represents URLs that have been changed and need to be replaced with a new URL.
<configSections> <sectionGroup name="RewriterConfig"> <section name="RewriterRules" type="ConfigRewriter.RulesHandler,RulesConfigHandler" /> </sectionGroup> </configSections> <!-- Custom Configuration Section for URL Rewriting --> <RewriterConfig> <RewriterRules> <Rule> <OldPath>/ideas/calculator.aspx</OldPath> <NewPath>/utilities/calculator.aspx</NewPath> </Rule> <Rule> <OldPath>/ideas/bmi.aspx</OldPath> <NewPath>/utilities/bmi.aspx</NewPath> </Rule> </RewriterRules> </RewriterConfig>
After the XML data structure is determined, the next step in developing a section handler is to create a class that is populated with the contents of the section and contains members that make the data available to an application. In this example, data is defined by any number of <Rule>
elements. Consequently, the class must provide some sort of collection to return multiple values. We'll use a Hashtable
for the purpose. The GetNewPath
method accepts a string value that it uses as a key to retrieve and return its associated value from the hash table.
// File: SectionRules.cs using System.Collections; namespace ConfigRewriter { // Class to contain content of configuration section public class RulesData { private Hashtable paths; // Constructor accepts hash table as parameter public RulesData (Hashtable pathCollection){ paths = pathCollection; } // Use old path as key and return hash table value public string GetNewPath(string OldUrl) { return ((string)paths[OldUrl]); } } }
The final step, shown in Listing 17-4, is to create the section handler code. Its primary function is to implement the Create
method. This method is passed an XMLNode
parameter that corresponds to the <RewriterRules>
node in our section. It is used to parse its child nodes and fill the hash table with its contents. An instance of the RulesData
class is instantiated by passing the hash table to its constructor. This object is then available to a Web application.
Example 17-4. Configuration Section Handler
//File: RedirectConfigHandler.cs using System.Xml; using System.Configuration; using System.Collections; namespace ConfigRewriter { public class RulesHandler: IConfigurationSectionHandler { public object Create(object parent, object input, XmlNode section) { Hashtable paths = new Hashtable(); string oldUrl=""; XmlElement root = (XmlElement) section; foreach (XmlNode node in root.ChildNodes){ foreach(XmlNode child in node.ChildNodes) { if(child.Name=="OldPath") oldUrl= child.InnerText; if(child.Name=="NewPath") { paths[oldUrl]= child.InnerText; } } } RulesData urlRules = new RulesData(paths); return urlRules; } } }
After the section has been constructed, accessing its content is trivial: use the ConfigurationSettings.GetConfig
method to return an instance of the section object, and use its properties to retrieve values.
// Retrieve object returned by section handler // and use it to access content of section RulesData r; r = ConfigurationSettings.GetConfig( "RewriterConfig/RewriterRules") as RulesData; if(r!=null) // Pass old path to method and receive redirected path string newURL = r.GetNewPath("/ideas/bmi.aspx");
Web-based applications must address many of the same security issues that are faced by applications situated on a local network. The most important of these—and the subject of this section—is client authentication. By this, we mean the capability to identify the user attempting to access a Web resource. Unlike traditional static Web sites that permit anonymous user access, an application that wants to tailor its content for the user, or restrict access based on authorization rights, must first authenticate the user.
ASP.NET offers three forms of client authentication:
Windows Authentication. Relies on the relationship between IIS (Internet Information Server) and the underlying operating system's directory security. Its options include using the native Microsoft Windows login authentication.
Microsoft Passport Service. Microsoft's proprietary “single sign-on” authentication service. Requires subscribing to the Passport network and installing the Passport SDK to link .NET to the service.
Forms Authentication. Allows an application to define user credentials and handle authentication based on it own requirements. This approach relies on .NET classes to create and manage a cookie that maintains user authentication information during the lifetime of a user's session.
The most flexible—and least proprietary—of these techniques is Forms Authentication. It represents the natural evolution from a do-it-yourself approach that requires creating a login screen, writing code to match user credentials against a database or file, and building a cookie to maintain client data during a session. ASP.NET offers substitutes for each of these tasks, which can be easily plugged into a developer's custom solution. The remainder of this section examines this form of authentication.
The general framework for implementing Forms Authentication is based on a mixture of configuration files, authentication modules, ASP.NET security classes, and the script (C# code) to work with the methods and properties of these classes. Figure 17-4 illustrates how it works.
A web.config
file defines a login page to which a user's browser is redirected when it first attempts to access a Web resource. This login page accepts the client's credentials—usually name and password—and determines if they are valid. If so, the
user is authenticated and ASP.NET methods are used to create a FormsAuthenticationTicket
that contains the user's security information. The ticket is encrypted and stored as a cookie. The cookie may be configured to expire at the end of the session, or remain on the client's machine so that the user can be automatically identified in future sessions. An optional step in this process is to add authorization information about a client to the ticket. For example, users may be assigned to groups that define their access rights. The code to add this role information can be placed in the global.asax
file to handle the application's AuthenticateRequest
event.
After these steps are complete and the user is authenticated, control passes to the Web page originally requested. Subsequent session authentication uses information in the authentication cookie and avoids these steps.
The web.config
file plays an important role in supporting authentication and authorization in ASP.NET security. Listing 17-5 presents many of its elements and optional attribute values that are used to select and implement a security method.
The first thing to note is the mode
attribute in the <authentication>
element, which specifies the type of authentication being used. Contained in the <authentication>
section is a <forms>
tag or <passport>
tag (not shown) that corresponds to the type of authentication chosen. The <forms>
tag, which is used in a later example, contains several key attributes. The loginurl
specifies the login page that a user is directed to when she has not been authenticated. The timeout
attribute specifies how long a user may wait between requests before the authentication ticket expires. Thus, if the value is set to 5 minutes and the user has not requested a page in that interval, she is forced to log in again at her next request. The protection
attribute specifies how, or if, ticket authentication information is processed before it is written to a cookie. You'll normally set this to All
.
The <forms>
element may contain a <credentials>
element that can be used as a mini-database to list each user and his password. The advantage of placing them here, as we see in a later example, is that .NET authentication classes provide methods to automatically perform authentication against these names. To add a measure of security for storing the passwords, the passwordFormat
attribute is provided to specify how the passwords are encrypted.
Web.config
may also contain an <authorization>
section that explicitly allows or denies access to users based on their name or role (think of role as a group the user belongs to). The <allow>
and <deny>
tags are processed in the order they occur in the file. When the tag's policy can be applied to the user, processing stops. If no allow or deny policy matches the user, the user is authorized to access the resource.
Example 17-5. Authentication and Authorization Configuration Sections
<Configuration> <system.web> <authentication mode="{Windows|Forms|Passport|None}"> <forms name="Cookiename" loginurl="path to login file" timeout="minutes" protection="{None|All|Encryption|Validation}" path="Cookiepath" > <credentials passwordFormat="Clear|MDS|SHA1}"> <user name="User Name" password="password" /> </credentials> </forms> </authentication> <authorization> <allow users="comma-separated list" roles="roles list" /> <deny users="comma-separated list" roles="roles list /> </authorization> ... </system.web> ... </configuration>
We'll now create a simple Web page that can only be accessed by users who provide a valid name and password. The example is then extended to demonstrate how client role information can be incorporated in the authorization process. As you work through the example, the most important things to observe are
The interrelationship between the web.config
file and authentication methods used in the login screen code.
How roles can be assigned to users by the use of global.asax
.
How to access authentication ticket information through the underlying .NET classes that manage authentication.
The configuration file segment in this example is used for both user authentication and authorization. The <credentials>
element contains the names and passwords of three users who may access the system. The <authorization>
element contains rules that deny login access to all anonymous users as well as one individual user. Only users Joanna and Ruth are permitted to access the Web page.
<system.web> <authentication mode="Forms"> <forms name="AppCookie" loginUrl="applogin.aspx" protection="All" timeout="10" path="/" > <credentials passwordFormat="Clear"> <user name="joanna" password="chicago" /> <user name="ruth" password="raleigh" /> <user name="kim" password="newyork" /> </credentials> </forms> </authentication> <authorization> <deny users="?,kim" /> </authorization>
The same authorization rights can be granted by a combination of allow/deny rules:
<allow users="joanna,ruth" /> <deny users="*" />
This denies access to all users (the asterisk (*
) wildcard selects all users) except for those that are explicitly allowed. When both <allow>
and <deny>
are present, the order in which they are placed can be important. In this example, placing the <deny>
rule first would deny access to all users, and the <allow>
rule would be ignored.
The web.config
file used in this example redirects the initial request for a Web page to applogin.aspx
, which accepts login credentials and performs user authentication. The login part is easy—two text boxes are used to accept a user name and password. The technique for authenticating the user is more interesting.
Traditionally, login screens validate a user-supplied name and password against a database or file containing valid users. You are still free to do this in ASP.NET, but there are other options. These include storing names and passwords in the web.config
file, or using the ASP.NET Membership
class and its preconfigured database to manage user credentials. For demonstration purposes, this example uses web.config
as the data store. Keep in mind that this is not the recommended approach if there are more than a few users, or multiple servers are used, which would require synchronizing the configuration file across machines.
As Listing 17-6 illustrates, the FormsAuthentication
class plays the key role in authenticating a user and creating the authentication ticket that identifies the user. This important class provides static properties and methods that are used to manipulate and manage authentication information. Two of its methods are used here: Authenticate
accepts a name and password, which it attempts to validate against the data in web.config
; RedirectFromLoginPage
redirects the user to the page originally requested, and creates an authentication cookie for the user. The second parameter to this method indicates whether the cookie should persist on the user's machine beyond the lifetime of the session.
Example 17-6. Login Screen Using ASP.NET Authentication
<%@ Page Language="C#" %> <html> <head> <title>login</title> <script runat="Server"> void verify_Form(object sender, System.EventArgs e) { // Code to verify user against a database goes here... // or use .NET to verify user using web.config info. // Name and password are compared against web.config content if( FormsAuthentication.Authenticate( txtName.Text,txtPW.Text)) { // Redirect to original form and // create authentication ticket bool persistCookie=false; FormsAuthentication.RedirectFromLoginPage(txtName.Text, persistCookie); } else { errmsg.Text="Cannot log in user."; } } </script> </head> <body > <form id=Form1 method=post runat="server"> <asp:Panel id="pnlName" style="Z-INDEX: 101; LEFT: 20px; POSITION: absolute; TOP: 64px" BackColor = "#efefe4" Height="120px" Width="278px" runat="server" > <TABLE> <TR> <TD><asp:Label id="lblName" Runat="server" text="User ID:"></asp:Label></TD> <TD><asp:TextBox id="txtName" Runat="server"> </asp:TextBox></TD></TR> <TR> <TD><asp:Label id="lblPW" Runat="server" text="Password:"></asp:Label></TD> <TD><asp:TextBox id="txtPW" TextMode="Password" Runat="server"> </asp:TextBox></TD></TR> <TR> <TD colspan=2 align=center> <asp:Button ID="btnSubmit" Text="Submit" Font-Bold="true" OnClick="verify_Form" Runat="server" /></TD></TR> <TR> <TD colspan=2 align=center> <asp:Label id=errmsg runat="server" /></TD> </TR> </TABLE> </asp:Panel> </FORM> </body> </html>
Authentication is often only the first step in permitting a user to access Web resources; the second step is authorization—the process of determining what resources the user is authorized to access. If all users have full access, this step can be ignored. However, if a user's access rights, or role, are determined by membership in a group, this group must be identified and associated with the user.
After a user is created in ASP.NET, information about that user is available through the Page.User
property. This property returns a principal object, so named because it implements the IPrincipal
interface. This interface has two important members (see Figure 17-5) that the principal object implements: Identity
, which can be used to get the name of the user, and the IsInRole
method that is used to check a user's group membership.
The IsInRole
method is passed a role name and returns a bool
value indicating whether the user is in that role. It is the developer's task to assign the roles to the authenticated user object so that this method will have an internal list to check against. Looking at Figure 17-5, you would expect this list to be part of the GenericPrincipal
object—and you would be right. The constructor for this class accepts as one of its parameters a string
array of roles to be associated with the user. To assign these roles, the application can take advantage of the AuthenticateRequest event that occurs when the identity of a user is established. A handler for this event is placed in the global.asax
file.
Listing 17-7 shows how code in the event handler assigns roles to the current user. It determines the user roles—in this case, calling GetRoles
—and places them in a string array. This array, along with the current Identity
, is passed to the GenericPrincipal
constructor to create a new object that updates the value of the User
property.
Example 17-7. Using global.asax
to Add Role Information for a User
<%! file:global.asax %>
<%@ Import Namespace="System.Security" %>
<%@ Import Namespace="System.Security.Principal" %>
<%@ Application %>
<script language = "C#" runat=server>
protected void Application_AuthenticateRequest(
object src, EventArgs e)
{
if (Request.IsAuthenticated)
{
string currUser= Context.User.Identity.Name;
string roles= GetRoles(currUser);
string[] appRoles = roles.Split(new char[]{'|'});
// Create GenericPrincipal class and add roles to it
// GenericPrincipal(IIdentity identity, string[] roles)
Context.User = new GenericPrincipal(
Context.User.Identity, appRoles);
}
}
private string GetRoles( string username )
{
// Code here would query database for user's role(s).
// Return role as delimited string that can split into array.
return "Administrator|Operator";
}
After the authentication steps are complete, an application may want to log information or make decisions based on information about the user. The User
property exposes all of the members shown in Figure 17-5. Further authentication details, such as when the authentication cookie expires or when it was issued, can be obtained from properties exposed by the authentication ticket. As shown in Listing 17-8, an instance of the cookie is created and then converted to a FormsAuthenticationTicket
object by using the Decrypt
method of the FormsAuthentication
class. The properties of this object are then displayed.
Example 17-8. Displaying Authentication Details
// Display authentication details Response.Write("Client: "+ User.Identity.Name+"<br>"); if(User.IsInRole("Operator")) Response.Write( "Role: Operator<br>"); string cookieName = FormsAuthentication.FormsCookieName; HttpCookie authCookie = Context.Request.Cookies[cookieName]; if(authCookie !=null) { // Create ticket from cookie and display properties FormsAuthenticationTicket authTicket = null; authTicket = FormsAuthentication.Decrypt(authCookie.Value); Response.Write("Issued: "+authTicket.IssueDate+"<br>"); Response.Write("Expiration: "+authTicket.Expiration+"<br>"); Response.Write("Persistent: "+authTicket.IsPersistent); }
The standard for the HTTP protocol is contained in a document known as RFC 2616. In printed form, it is more than 120 pages in length. If you search this document, you'll find that not one page, section, or sentence discusses a method for maintaining state information between HTTP requests. The term cookie is completely absent. So, unlike human memory where “our thoughts are linked by many a hidden chain” (Alexander Pope), the Internet protocol is designed to be stateless—each request unlinked to the preceding one.
Yet, for Internet applications to flourish, a means has to exist to identify specific users. Online purchasing, surveys, and the need to recognize user preferences depend on it. The first efforts to maintain state information relied on using features
defined in the HTTP protocol: hidden fields that could be sent back and forth using the POST
method, and a string full of values that could be placed after the ?
character in an HTTP URL path. The former technique is used to maintain .NET ViewState
information; the latter constitutes a “query string” consisting of name/value pairs.
A third client-side approach to managing state is the use of cookies, a small (4K) text-only string that is stored in browser memory during a session and may be written to disk. Many applications maintain session-related state information, such as a shopping cart's identification, in cookies. ASP.NET identifies each session by creating a cookie containing a unique session ID.
These approaches are plagued by a common weakness: They can be compromised by the client. Query strings and hidden fields can be altered; cookies can be altered or simply turned off on the browser.
To overcome these shortcomings, ASP.NET offers two general types of server-side state management: session state, which maintains information for the life of the session; and application state, which maintains information across multiple sessions. Although these techniques maintain information on a server—rather than passing it back and forth between requests—they still rely on a cookie, where possible, to uniquely identify a user's session. However, the server-based solution presents another problem: All session activity must be handled by that server possessing the state information. This is incompatible with large Web sites where a server-selection algorithm assigns requests to any machine in a Web farm. An ASP.NET solution to this is the use of a central database to store session state information.
Table 17-4 summarizes the client- and server-side state management approaches. Of those listed, this section focuses on the application and session management techniques.
Table 17-4. State Management Techniques for Web Applications
Technique | Web Farm Compatible | Description |
---|---|---|
Query String | Yes | Data passed as part of URL. |
View State | Yes | ASP.NET hidden |
Cookie | Yes | Information passed to/from browser in a cookie. |
Application | No | An |
Session: In-Process | No | Session state is stored and managed using the |
Session: State Server | Yes | Session state is stored in the |
Session: SQL Server | Yes | State information is maintained in a temporary table in a database. |
Application state data is maintained in an instance of the HttpApplicationState
class—an in-memory dictionary holding key-value objects. The contents of this dictionary object are available to all sessions using that application.
The data is usually loaded within the global.asax
file when the Application_ Start
event fires.
protected void Application_Start(object sender, EventArgs e) { Application["targetBMI"]= 21; Application["obeseBMI"] = 26; }
Users of the application then have read and write access to the data without having to regenerate it. This is particularly useful when working with data from a database. It can also be a convenient way to initialize variables used for collecting usage statistics. (See Section 17.2 for background on global.asax
file.)
All sessions using the application may access the Application
object through the Application
property of the System.Web.UI.Page
type. The only thing to note about accessing data is that it must be cast, because the information is stored as an object. Here is a segment that illustrates the primary properties and methods for working with the Application
object:
int target = (int) Application["targetBMI"]; // Change value Application["targetBMI"] = 22; // List all HttpApplicationState values foreach( string s in Application.AllKeys){ Response.Output.Write("<br>{0} = {1}",s,Application[s]); } // Remove Application item Application.Remove("obeseBMI");
Not illustrated are the Lock
and UnLock
methods that allow application variables to be updated in a thread-safe manner.
The variables contained in application state have two primary uses: as read-only data globally available across all of the application's sessions, and as variables that are updated to keep statistics regarding the application's use. Both of these uses can be handled more efficiently using a different approach.
The ASP.NET data cache
provides a more flexible approach for making read-only data available on an application-wide basis. Its contents are as accessible as those in the application object, and it offers the advantage of having its contents automatically refreshed at a specified time interval. (Its use is described in Section 17.5.) As a rule-of-thumb, use application state to preload small amounts of read-only data required by an application; use the data cache
when working with large amounts of data.
The problem with maintaining usage data in application state is that the data is available only during the life of the application. When it ends, the data is gone. In addition, if there are multiple servers, the data will apply to only one server. A better approach is store the information in a central database.
Application recycling refers to shutting down and restarting an application based on criteria such as time, memory usage, and number of client requests. It is set using the <processModel>
tag in the web.config
file. The following setting recycles the application every two hours.
<processModel timeout="120">
When a new client requests an ASP.NET application, ASP.NET creates a cookie containing a unique session ID and returns it to the client's browser. This ID, or session key, is used to identify subsequent requests from the client. State information unique to each session is maintained in an instance of the HttpSessionState
object. The contents of this object are available through the Session
properties of both the Page
and HttpContext
class. Note that Session
can be used in place of the fully qualified HttpContext.Session
. Working with session state is analogous to working with application state. Many applications perform state session initialization in a Session_Start
event handler in the global.asax
file. This segment initializes variables for an online survey:
// global.asax file protected void Session_Start(object sender, EventArgs e) { Session["Start"]= DateTime.Now; Session["QuestionsAnswered"] = 0; }
The variables can be updated in the application with the same syntax used for the Application
object:
int ict= (int)Session["QuestionsAnswered"]+1; Session["QuestionsAnswered"]=ict;
The HttpSessionState
class contains several members that are useful in manipulating and interrogating the Session
object. The following code demonstrates their use:
// Session ID is unique to each session Response.Write("<br>Session ID: "+Session.SessionID); // Mode may be InProc, StateServer, SqlServer, Off Response.Write("<br>Processing Mode: "+Session.Mode); // Session times out after 20 minutes (default) of inactivity Response.Write("<br> Timeout: "+Session.Timeout); // Number of items in session state Response.Write("<br>Items: "+Session.Count); // List key-values in session state foreach( string key in Session.Keys) Response.Output.Write("<br>{0} = {1}",key, Session[key]);
The most interesting of these properties, by far, is Session.Mode
, which indicates where the Session
object is configured to store its data. Session state information can be stored using one of three methods: InProc
, StateServer
, or SqlServer
. Although the choice does not affect the way information is accessed through the Session
object, it does affect application performance, manageability, and extensibility. Let's look at the details of using all three backing stores (see Figure 17-6).
By default, ASP.NET stores session state in-process using the aspnet_wp.exe
process. This means that the information is managed in memory space where the application is running. This has the advantage of providing fast access to the data. The drawbacks are that it must run on a single server—precluding its use with Web farms—and that all session data is lost if the server reboots. Given this, it is recommended for single-server sites that need to maintain moderate amounts of data in state.
To select in-process session state management, set the mode
attribute to InProc
in the <sessionState>
element of the web.config
file. As shown here, you can also specify a time limit specifying how long the process may be idle before the session data is discarded.
<configuration> <system.web> <sessionState mode="InProc" timeout="30"/> </system.web> </configuration>
The concept of out-of-process session state management entails the use of a mechanism outside of the application's process to store state data. ASP.NET provides two such mechanisms: a separate process running on a selected state server and the use of a data server to store state information.
When session state mode is used, session state is stored in the Aspnet_ state.exe
process. This allows an application running on multiple servers to share state information. Because the server running this process is accessed via a network on the Internet, performance is slower than using the in-process approach. Also, the data is susceptible to a server reboot.
To access a state server, make sure the machine selected is running the Aspnet_state.exe
process. Any server accessing the state server must set the <sessionState>
element in its web.config
file to point to the state server. The default port used is 42424. On a Windows-based operating system, this can be changed through the registry.
<configuration> <system.web> <sessionState mode="StateServer" stateConnectionString="192.168.1.109:42424" /> </system.web> </configuration>
Using a SQL Server database to store session state data also offers the capability of sharing data among machines. In addition, SQL Server security features can be used to selectively provide access to the data; and, of course, the data is not transient because it exists in tables. The disadvantages of this approach are that it only works with Microsoft SQL Server and it is the slowest of the three session state modes.
To prepare a machine running SQL Server to handle session state requires no more than creating the tables that ASP.NET expects. The InstallPersistSqlState.sql
script takes care of this. By default, the script is located in the systemrootMicrosoft.NETFramework
version
folder. It creates a database named ASPState
that contains the state tables.
Web.config
is used to tell ASP.NET that session information for the application(s) is stored on a SQL Server. The <sessionState>
element is set to specify the mode
and connection string.
<configuration> <system.web> <sessionState mode="SQLServer" sqlConnectionString="datasource=x; user id=sa; password="/> </system.web> </configuration>
SQL Server should be considered when there is a need to maintain data beyond the life of the session. For example, sites often persist the content of a shopping cart so it is available when the user logs in again.
Because the use of SQL Server slows Web page performance, it should be used only on those pages in an application that require state data. The @Page
directive has an EnableSessionState
attribute that dictates how state is used on the page. It can be set to false
to disable session state, true
to enable read and write session state, or readOnly
.
Consider a Web page with a list box containing the names of a thousand movie actors whose movies are displayed when an actor is selected. The first time the page is requested, a query is sent to the database, the list box is populated with the results, and the rendered HTML is returned to the user's browser. This is a relatively slow and expensive process. Multiply it by a thousand user requests and Web server response time will slow measurably.
One solution is to use caching to bypass most, or all, of this reprocessing by keeping an HTML page or data in memory. Subsequent requests for the page are handled using the cached information—obviating the need to fetch data from the database. A sound caching strategy can improve Web server response more than any other single factor. This section presents the factors to be weighed in designing a caching strategy and the two broad categories of caching available in ASP.NET: output caching and data (or request) caching.
ASP.NET permits a developer to indicate whether all or part of a Web Form should be cached, where it should be cached, and how long it should remain in the cache before it expires and must be refreshed. The key to cache control for a Web Form is the @OutputCache
directive:
<%@OutputCache Duration="30" Location="Any" VaryByParam="none" %>
Its attributes declare the specific caching policies in effect for that Web page. Let's look at how these attributes are used.
ASP.NET processes the @OutputCache
directive by translating its attributes into HTTPCachePolicy
method calls.
The mandatory Duration
attribute specifies the length, in seconds, that the page or control is cached. Setting this to 20 seconds generates the underlying statement:
Response.Cache.SetExpires(DateTime.Now.AddSeconds(20));
The Location
attribute specifies where the caching can occur. In general, caching occurs on a server, browser, or somewhere in between, such as on a proxy server. The values of this attribute correspond to these locations: Any
, Client
, DownStream
, Server
, ServerAndClient
, or None
. DownStream
refers to a cache-capable device other than the origin server. None
disables caching everywhere.
In Chapter 16, we developed a BMI calculator Web page in which the user enters a height and weight value and the BMI value is returned. Because there are thousands of combinations of height (htf
, hti
) and weight (wt
) parameters, several slightly different versions of the same page are likely to be created. The VaryByParam
attribute is available for cases such as this. Its value indicates the parameter(s) (from a POST
or query string) that should be considered when selecting a page to cache. For example, the following statement causes a page to be cached for each unique combination of wt
, hti
, and htf v
alues:
<%@ OutputCache Duration="60" VaryByParam="wt;hti;htf" %>
Figure 17-7 shows four requests that result in three unique pages being cached with their calculated BMI value. Note that we could have also assigned an asterisk (*
) to VaryByParam
to indicate that each parameter's value affects the cache.
Be aware that you can create some unexpected results if you set VaryByParam
incorrectly. If only the wt
parameter were specified in this example, requests with a weight of 168 and heights of 5'1" and 6'1" would return the same Web page. The page returned to both would be the one requested earliest.
VaryByHeader
caches a different version of a page, based on the value of the Request Header
fields (refer to Figure 17-1). This is the most commonly used with the Accept-Language
field to ensure that different language versions of a Web page are cached.
The final conditional attribute to be familiar with is VaryByCustom
. This is most useful when you have a Web page that is rendered differently depending on the client's browser type and version. To create different page versions by browser type, set the VaryByCustom
attribute to "Browser"
.
<%@ OutputCache Duration="60" VaryByParam="*" VaryByCustom="Browser" %>
This attribute also can be set to recognize custom values that your program generates. Please refer to caching documentation for details on this.
All parts of a Web page are not created equally. Some, such as headers and links, rarely change, whereas other sections, such as the latest stock exchange quotes, change by the minute. A desirable caching strategy is to identify the static objects on the page and cache only those. This can be done in ASP.NET by creating custom controls (.ascx
files) for the sections of the Web page to be cached. Custom controls have their own OutputCache
directive that determines how they are cached, irrespective of the form containing them.
To illustrate the concept, let's create a simple control to be embedded within a Web page (see Figure 17-8). This segment simply contains a couple of links to other Web sites.
<!-- File: CacheFrag.ascx --> <%@ OutputCache Duration="30" VaryByParam="None" %> <b>My Links</b><br> <a href=http://www.moviesites.org>Movies</a><br> <a href=http://www.imdb.com>Movie Reviews</a> <br>
The application Web page to hold this control displays a title and date. Note that its caching is turned off by setting Location
to None
. The result is that only the control is cached.
<!-- File: CacheMain -->
<%@ Page Language="C#" %>
<%@ OutputCache Location="None" VaryByParam="*" %>
<%@ Register TagPrefix="frag" TagName="fraglinks"
Src="CacheFrag.ascx" %>
<HTML>
<HEAD><TITLE>Sample Web Site</TITLE>
<body>
<center> <b>Fragment Sample</b>
<br>
<% Response.Output.Write(DateTime.Now.ToString("f")); %>
</center>
<frag:fraglinks id="fragmentlink" runat="server" />
</body>
</html>
Data caching, sometimes referred to as request caching, is often an alternative to using application state or ViewState
to store read-only data. It is as easy to use as the Application
object, and adds the flexibility of assigning expiration and priority values to the stored data.
The simplest way to add data to a data cache is to use the key-value syntax of the Cache
property of the Page
class:
Cache["userid"] = "rigoletto";
The Cache.Insert
method is often a better approach since it takes full advantage of the fact that each cache entry is actually an instance of the CacheEntry
class. This class contains many useful properties that are set by passing parameters to the Insert
method:
Syntax: | Example: |
---|---|
public void Insert( string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallBack cb ); |
Cache.Insert ( "userid", "rigoletto", null, DateTime.Now.AddMinutes(20), TimeSpan.Zero, CacheItemPriority.Normal, null ); |
Either the absoluteExpiration
or slidingExpiration
parameter can be used to determine when the cached data expires. The former sets a specific date and time; the latter sets the expiration to occur a specified amount of time after the value is last accessed. To disable sliding expiration, assign the value Cache.NoSlidingExpiration
.
The dependencies
parameter allows cached data to be tied to files, directories, or other objects in the cache. Establishing this dependency causes the cached data to be removed when the object it is dependent upon changes. Thus, if cached data comes from a file and the file contents are modified, the data in the cache is removed.
string filename = "actors"; CacheDependency dep = new CacheDependency(fileName); cache.Insert("key", "value", dep);
The priority
parameter makes sense when you understand that data in a cache is not guaranteed to remain there. Because there is no limit on what can be placed in the cache, ASP.NET periodically invokes a resource scavenging process to remove less important data from the cache. The determination of what is important is based on the priority of the resource. By setting this priority to a CacheItemPriority enum
value, you can reduce, or increase, its likelihood of being removed through scavenging. Enum
values may be set to AboveNormal
, BelowNormal
, Default
, High
, Low
, Normal
, and NotRemovable
.
To retrieve data from the cache, simply specify the key (case is ignored) of the desired data:
String user = (string) Cache["userid"];
Because the data is stored as an object, it is necessary to cast the result. If no data exists for the key, a null
value is returned.
The true value of data caching comes into play when large amounts of data need to be saved to restore a page's state. As was explained in the previous chapter, ViewState
automatically saves data and state information for Web controls on a page as long as the page posts to itself. Figure 17-9 illustrates this. In it, we have Web page A containing a list box populated with several hundred items from a database. In steps 1 and 2, a user performs some action that results in a modified version of the same page being returned to the client. State is retained automatically because the contents of the page's list box—and any other fields—are passed in the ViewState
field.
Suppose the user selects an item in the list box on page A that requires loading page B to show more details about the item. The server constructs the description as page B and returns it to the browser, discarding page A. After viewing this page, the user now wants to return to A. In the absence of caching, page A must be reconstructed by retrieving data from the database. To avoid this, the contents of the list box can be cached prior to navigating to page B (step 3):
// Step 3: Save listbox data before making server request Cache["listbox"]= actors.Items;
In step 6, page A is constructed using data from the cache to restore the list box.
// Step 6: Fill list box from cache if(Cache["listbox"] !=null){ ListItemCollection li= (ListItemCollection)Cache["listbox"]; for(int i=0;i< li.Count; i++){ ListItem a= li[i]; actor.Items.Add(a); } } Cache["listbox"] = null; // Clear cache
The cache is cleared after the page is restored; otherwise, both the cache and view state will contain the contents of the ListBox
. It is worth noting that another option is to turn view state off for the ListBox
(EnableViewState=false
) and rely on caching to maintain its content.
As you would expect in a chapter on ASP.NET, the role of the Web client does not receive much attention. It's usually assumed to be a browser that requests a Web page specified by the user, and renders the returned HTML content. Despite the evolution of browser features, its primary role remains to display what it receives. It is of less use if you want to parse and extract portions of the received content—or want to examine the HTTP message headers. For this, you need a Web client—a program that communicates with an HTTP server and includes custom methods to perform operations on the response object. This section demonstrates how easy it is to write a Web client using .NET classes to handle the HTTP request/response duties.
The System.Net
namespace contains classes that are intended for applications involved with network communications. Included in these are the abstract WebRequest
and WebResponse
classes that contain generic properties to upload and download data given a specific Uniform Resource Identifier (URI). These classes are designed to support the response/request model—not a specific protocol. The details of that are left to descendant classes. To handle the HTTP protocol, we call on the HttpWebRequest
and HttpWebResponse
classes. Note that other classes are available to support the file://
URI scheme and FTP operations.
This example accepts a URL, sends a request for its Web page, and uses the response object to display the server description, IP address(es) of the server, and source code for the requested Web page. Figure 17-10 shows the interface for the application.
The basic steps for communicating synchronously with the HTTP server are quite simple:
Create an HttpWebRequest
object that identifies the URL using the Create
method:
request = (HttpWebRequest) WebRequest.Create(url);
Get a response from the Internet resource using the WebRequest.GetResponse
method:
response = (HttpWebResponse) request.GetResponse();
Get the Stream
used to read the body of the response and read its contents.
Stream s = response.GetResponseStream(); string strContents = new StreamReader(s).ReadToEnd();
Note that request and response are cast to HttpWebRequest
and HttpWebResponse
types, respectively. As mentioned, these subclasses deal specifically with the HTTP protocol (see Listing 17-9).
Example 17-9. Using WebRequest
and WebResponse
to Scrape a Web Page
private void btnURL_Click(object sender, System.EventArgs e) { // Fetch web page for requested URL HttpWebRequest request; HttpWebResponse response; if(txt_URL.Text.Length>0) { lblServer.Text=""; tbIP.Text=""; string serverPath= txt_URL.Text; string url="http://"+serverPath; // create a request to the url request = (HttpWebRequest) WebRequest.Create(url); request.Timeout= 7000; // timeout after 7 seconds try { response = (HttpWebResponse) request.GetResponse(); lblServer.Text= response.Server; // Get a stream to send the web page source Stream s = response.GetResponseStream(); string strContents = new StreamReader(s).ReadToEnd(); // Place Web page source in text box HTMLViewer.Text= strContents; s.Close(); ListIP(serverPath); // List IP address(es) } catch ( Exception ex) { lblServer.Text= ex.Message; } } else { lblServer.Text= "Please enter a domain name."; } } private void ListIP(string uri) { // List IP addresses for this domain // Use only server name part of URI for IP resolution int ndx= uri.IndexOf("/"); if(ndx>0) uri= uri.Substring(0,ndx); string ips=""; // Get a list of IP addresses for the URI // Dns contacts the Internet Domain Name System IPHostEntry IPHost = Dns.GetHostByName(uri); foreach(IPAddress addr in IPHost.AddressList) ips+= addr.ToString()+" "; tbIP.Text= ips; }
You may encounter Web pages that check the UserAgent
property of the request object and do not allow their pages to be downloaded unless they can identify the browser. You can assign a legitimate browser value to overcome this.
The purpose of this section is to provide background information about the components and classes that come into play between the time a Web server receives a request and the time it finishes crafting a response. An understanding of this HTTP pipeline can be crucial to improving Web application performance, because it enables a developer to create components that affect how ASP.NET responds to requests. The references to HTTP modules and handler factories can make the subject appear daunting, but underneath there is a simple logic at work that is rooted in event handling.
As a request makes its way to a server, several events occur. ASP.NET permits a developer to interact with the request or response at the point of these events—through special code referred to as an HTTP module or by script in a global.asax
file. Both the module and file function as event handlers, and the nature of what goes on in these event handlers is the topic of this section. Figure 17-11 depicts how a request flows from the beginning of the HTTP pipeline to the endpoint, where a handler provides a response.
Let's take a simplified look at the sequence of events that occur.
Request is received by Web server. The HTTP request for the bmi.aspx
resource is passed to the ASP.NET Worker Process (aspnet_wp.exe
). It creates an instance of the HttpRuntime
class, which initiates processing of the request.
The processing begins with the creation of an HttpContext
object. You can think of this as the central repository of all information pertaining to the request. When another class needs information about the request, it can find it in an HttpContext
field or property. In addition to the HttpRequest
and HttpResponse
fields, the class also exposes state information through HttpSessionState
and HttpApplicationState
properties.
HttpApplicationFactory
provides the HttpApplication
object to process the request. After the HttpContext
object is created, the HttpRuntime
calls on the static factory method HttpApplicationFactory.GetApplicationInstance
to create or find an HttpApplication
object to process the request.
HttpApplication
is initialized with HTTP modules that filter request and response. HTTP modules are used to process a request and response as they pass through the pipeline. Predefined ASP.NET modules provide authentication, caching, and session state services.
Locate handler factory. After the HttpApplication
object takes over the processing from the HttpRuntime
, it locates the HandlerFactory
that finds or creates the HTTP handler object. It does this by examining the type of request (.aspx
) and searching the machine.config
file or web.config
for the name of the handler
that maps to this request type. Different handlers are needed for the different types of resources that may be requested. For example, a request for a Web page is handled differently than one for a text or XML file.
Handler processes request. In our example, the handler class is the requested .aspx
page. It may seem strange that the page is a handler, but the main requirement for a handler is that it implement the IHttpHandler
interface—which the .aspx
page does via its Page
class.
Response is constructed and returned. The ProcessRequest
method of the handler is called to generate the response. This method takes an HttpContext
object parameter that gives it access to the requested information needed to complete the processing.
As requests and responses move through the HTTP pipeline, an ordered chain of deterministic events—starting with the HttpApplication.BeginRequest
event and concluding with the HttpApplication.EndRequest
event—defines each stage of the processing. In addition, random error events can arise at any time from unhandled exceptions.
A developer can greatly improve the robustness and effectiveness of an application by selectively developing handlers for these events. Handlers are typically used to trap error events and direct them to a custom error page, log session lengths and number of users, authenticate users, and adorn pages with common header and footer information. These events can be handled using either a custom application class or custom HTTP modules. We'll look at both, and see how they compare.
A third place to customize the pipeline is at the endpoint, where an HTTP handler processes a resource request. As we will see, different resources, such as .aspx
or .soap
files, use their own handler components. ASP.NET makes it easy to write your own handler to manage special resources
The HttpApplication
object handles most of the duties required to process a request. Its properties expose caching and application/session state information, as well as a collection of predefined HttpModules
. Equally important are the set of events it exposes. We can customize an application by including code in a global.asax
file to handle these events.
Table 17-5 lists the HttpApplication
events in the order that they occur in the HTTP pipeline. The Error
event is an exception because it can occur at any time during the process.
Table 17-5. HttpApplication
Events
Event | Description |
---|---|
| Fires when ASP.NET receives a request. A request can be modified here before it reaches the page. |
| The identity of the user has been established. |
| User authorization has been verified. |
| Fires when ASP.NET determines whether to handle request from a cached page or pass request to a handler. |
| Acquires the state of the current session. |
| Fires before the request is sent to a handler. In the case of a request for an |
| Fires after the handler has packaged a response. The response object contains the text returned to the client. |
| Fires after all request handlers have been executed and ASP.NET is ready to store session state information. |
| Data being sent to client is stored in the cache for future requests. |
| Occurs when request has been processed. |
| Occurs in response to unhandled application exception. |
Your code can provide a handler for these events by creating a component that uses a delegate to subscribe to the event or define event handlers in the global.asax
file. We'll demonstrate the former approach when discussing HTTP modules, but first let's look at how to use the global.asax
file.
This file is stored in the root of the virtual directory containing the application(s) it is to be associated with. Its primary purpose is to hold the handlers that respond to the Application
object's events. It is similar to an .aspx
file in that it is compiled into an assembly when the Web application is first accessed. Unlike the Web page, however, it inherits from the HttpApplication
class.
Here is a simple global.asax
file that displays copyright information at the bottom of each page returned by an application in its virtual directory. It does this by implementing a custom handler for the EndRequest
event. Notice that the format of
the event handler for this, and all Application
events in the global.asax
file, is Application_
eventname
.
<%! file:global.asax %>
<%@ Application Language="C#" %>
<script runat=server>
protected void Application_EndRequest(object sender, EventArgs e)
{
this.Context.Response.Output.Write(
"<br>©2005 BMI Software");
}
</script>
Global.asax
supports four other important events that are not members of the HttpApplication
class: Application_Start
, Application_End
, Session_Start
, and Session_End
. The purpose of these events, which should be clear from their name, is to mark the beginning and end of an application and the sessions using it. They are the most frequently implemented event handlers in the global.asax
file—performing the bookend operations of state initialization and cleanup. To illustrate this, let's extend the preceding global.asax
example to keep track of the number of sessions that request an application and display this session number below the copyright.
The session counters are kept in an HttpApplicationState
object represented by the Application
property of the HttpApplication
base class. It acts like a dictionary to hold information during the lifetime of an application (refer to Section 17.4, “Maintaining State”).
As shown in Listing 17-10, counters that track the number of sessions and sessions cancelled are initialized to zero when the Application_Start
event fires. The session counter is incremented when a Session_Start
occurs, and the counter for terminated sessions is incremented when a Session_Ends
event occurs.
Example 17-10. Using global.asax
to Count Sessions
<%! global.asax %> <%@ Application Language="C#" %> <script language = "C#" runat=server> // Can also use Application_OnStart syntax void Application_Start(Object Sender, EventArgs e) { // Initialize counters with application scope Application["Sessions"] = 0; Application["TerminatedSessions"] = 0; } void Session_Start(Object Sender, EventArgs e) { Application.Lock(); Application["Sessions"]=(int) Application["Sessions"] + 1; Application.UnLock(); } void Session_End(Object Sender, EventArgs e) { Application.Lock(); Application["TerminatedSessions"] = (int) Application["TerminatedSessions"] + 1; Application.UnLock(); } protected void Application_EndRequest(object sender, EventArgs e) { string sessionCt= "Session #: "+Application["Sessions"].ToString(); // Display copyright and current session number this.Context.Response.Output.Write( "<br>©2005 BMI Software<br>"+sessionCt); } </script>
As with the .aspx
file, code can be separated from the global.asax
file and placed in a code-behind file. The code is compiled into a DLL and placed it in the in
subdirectory of the application. The Inherits
attribute of the Application
directive points to the class implemented in the DLL.
To convert the preceding example to use a code-behind file, we remove code from the global.asax
file, leaving only the Application
directive:
<%! file:global.asax %> <%@ Application Inherits="sessionctr" %>
The code-behind file must be implemented as a class that inherits from HttpApplication
. Here is a segment of the code:
// file: sessionctr.cs using System; using System.Web; public class sessionctr: HttpApplication { void Application_OnStart(Object Sender, EventArgs e) { Application["Sessions"] = 0; Application["TerminatedSessions"] = 0; } // Remainder of assembly code goes here }
An HttpModule
is a component that implements the IHttpModule
interface. The role of the module is similar to that of the global.asax
file: It processes events (refer to Table 17-2) associated with response and request messages in the HTTP pipeline. In many cases, you can implement the same functionality using either the module or global.asax
file. We discuss the factors that affect your choice later in this section.
Listing 17-11 shows the predefined modules that ship with ASP.NET. The physical modules (assemblies) are stored in the Global Assembly Cache (GAC) and are made available by listing them in the <httpModules>
section of the machine.config
file. They perform such diverse tasks as user authentication, caching, and state management. Although you do not work with the modules directly, they should give you an idea of the type of pre- and post-request processing that can be performed.
Example 17-11. HTTP Modules in machine.config
File
<httpModules> <add name="OutputCache" type="System.Web.Caching.OutputCacheModule" /> <add name="Session" type="System.Web.SessionState.SessionStateModule" /> <add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule" /> <add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" /> <add name="PassportAuthentication" type="System.Web.Security.PassportAuthenticationModule" /> <add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" /> <add name="FileAuthorization" type="System.Web.Security.FileAuthorizationModule" /> <add name="ErrorHandlerModule" type="System.Web.Mobile.ErrorHandlerModule, System.Web.Mobile, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" /> </httpModules>
There are two minimal requirements for building a functioning HttpModule
: It must contain a class that implements the IHttpModule
interface and it must register an event handler to process one of the HttpApplication
events. An example of this is shown in Listing 17-12. The code adds copyright information to the bottom of all pages returned by the application—exactly the same function performed by the earlier global.asax
file.
The IHttpModule
interface consists of two methods—Init
and Dispose
—that must be accounted for. We use Init
to register the event handler that will be called when the EndRequest
event occurs. Note that access to the events is provided through the HttpApplication
object that Init
receives as a parameter.
Example 17-12. Module to Append Copyright to Web Pages
using System; using System.Web; namespace CustomModules{ public class FooterModule: IHttpModule { public void Init(HttpApplication httpApp) { httpApp.EndRequest += new EventHandler(this.OnEndRequest); } public void Dispose() {} // public void OnEndRequest (Object obj, EventArgs e) { HttpApplication httpApp = (HttpApplication) obj; httpApp.Context.Response.Output.Write( "<br>©2005 BMI Software"); } } }
To use the module with a Web application, compile it and place it in the in
subdirectory of the application or in the Global Assembly Cache (GAC). Next, the assembly must be registered in either the web.config
or machine.config
file. In this example, we use a web.config
file, which is placed in the root of the application's virtual directory. A portion of the file is shown here:
<configuration> <system.web> <httpModules> <add name="copyright" type="CustomModules.FooterModule, footmodule" /> </httpModules> </system.web> </configuration>
The key part is the <add>
element that takes the form:
<add name=friendly name type=class name, assembly />
This element is enclosed by the <httpModules>
elements. Multiple <add>
elements can be used to specify multiple modules.
URL rewriting is the process of changing a requested URL so that it is redirected to another resource. This is commonly used to ensure that bookmarked links continue to work when directories and resource names are changed on a Web server.
An HTTP module provides a convenient and easy-to-use approach for rewriting a URL. The implementation logic in the module is straightforward:
An HttpApplication
event invokes event handler code.
The event handler examines the Context.Request.Path
property for the requested path.
If the path URL needs to be redirected, the Context.RewritePath
method is called with the substitute path.
The most important issue to be resolved in developing a rewriter is selecting an event in the request's lifecycle where the URL checking and modification should occur. The three HttpAppliction
event candidates are shown in Table 17-6, along with the criteria for selecting them. (Authentication is discussed in Section 17.5.)
Table 17-6. Event Choices for Implementing a URL Rewriter Module
Event | Criteria for Selecting Event |
| Use unless forms authentications is also used. |
| Use if Windows authentication is used. |
| Use if Forms Authentication is used and Windows authentication is not. |
In general, if no authentication is being used, place the rewrite logic in an event handler for the BeginRequest
event.
Listing 17-13 contains code for a simple rewriter module with the rewriting logic included in the AuthorizeRequest
event handler. Look closely at this code. It first calls a static method Rewrites.getRewrites()
that returns a hash table in which the keys are old URLs and the associated value is the replacement URL. The hash table is checked to see if it contains the currently requested path and if it's in the table, a call is made to Context.RewritePath
. The new URL is passed to it, and the method automatically takes care of details such as including any query string with the new URL.
Example 17-13. Custom HTTP Module to Rewrite a URL
using System;
using System.Web;
using System.Collections;
namespace CustomModules
{
public class ReWriteModule: IHttpModule
{
public void Init(HttpApplication httpApp)
{
httpApp.AuthorizeRequest += new
EventHandler(this.OnAuthorizeRequest);
}
public void Dispose() {}
// Determine if requested URL needs to be rewritten
public void OnAuthorizeRequest( Object obj, EventArgs e)
{
// Get hash table containing old and replacement URLs
Hashtable urls = Rewrites.getRewrites();
HttpApplication httpApp = (HttpApplication) obj;
// Get path of requested URL
string path = httpApp.Context.Request.Path.ToUpper();
// See if path is in hash table
if(urls.ContainsKey(path))
path= (string)urls[path];
httpApp.Context.RewritePath(path); // Rewrite URL
}
}
}
The module is registered by placing the following entry in the web.config
file:
<add name="redirector" type="CustomModules.ReWriteModule, rewritemodule" />
The process of identifying URLs to be rewritten can be implemented in various ways. This example stores the old and replacement URLs in an XML file using the following format:
<!-- File: redirector.xml --> <RewriterConfig> <RewriterRules> <Rule> <OldPath>/ideas/calculator.aspx</OldPath> <NewPath>/utilities/calculator.aspx</NewPath> </Rule> <Rule> <OldPath>/ideas/bmi.aspx</OldPath> <NewPath>/utilities/bminew.aspx</NewPath> </Rule> </RewriterRules> </RewriterConfig>
This information could also be stored in the web.config
file, although some overhead is required to do this. For details, refer to “Adding a Custom Configuration Section” on page 824.
In many cases, the functionality you want to add to a Web server environment can be implemented using HTTP modules or the global.asax
file. As a guide to making the choice, let's review their features:
Both can contain handlers to service HttpApplication
events.
Global.asax
can receive notification of session
and application
start and end events. An HTTP module cannot service these events.
Modules are deployed at the machine (machine.config
) or virtual directory (web.config
) level; global.asax
operates at a directory level only.
Either can be used to handle HttpApplication
events. If the feature being added applies to all applications on the server, a module is the better choice; if the feature is specific to an application, use a global.asax
file.
When a request is made for a resource, ASP.NET directs control to the appropriate handler for that resource. For example, if the request is for an .aspx file
, control passes to the PageHandlerFactory
component, which returns the requested .aspx
page. The default ASP.NET handlers are registered in the machine.config
file within the <httpHandlers>
section. An extract of that file shows how the resource type is mapped to the class capable of creating a handler for it:
<httpHandlers> <add verb="*" path="trace.axd" type="System.Web.Handlers.TraceHandler" /> <add verb="*" path="*.aspx" type="System.Web.UI.PageHandlerFactory" /> <add verb="*" path="*.ashx" type="System.Web.UI.SimpleHandlerFactory" /> <add verb="GET,HEAD" path="*" type="System.Web.StaticFileHandler" /> ... </httpHandlers>
An HTTP handler is a component that implements the System.Web.IhttpHandler
interface. Unlike an HTTP module, only one handler is called to process a request. ASP.NET maps a request to the target handler based on information in a machine.config
or web.config
file. The request can be mapped directly to a handler or to a handler factory that creates or retrieves the appropriate handler. As shown in the preceding machine.config
file, .aspx
requests are handled by a PageHandlerFactory
class. We'll look at both techniques.
ASP.NET provides handlers for .aspx
, .soap
, .asmx
(Web services), and other standard ASP.NET file types. So why create a custom handler? The primary motivation is to support new file extensions or existing file extensions that require added
processing features. For example, when a text file (.txt
) is requested, ASP.NET simply returns the contents of the file. As an exercise, let's improve this with a handler that accepts a URL with the name of the text file and a query string parameter that specifies a keyword to search for in the file. The output from the handler is a listing of the text file with all instances of the keyword highlighted, as well as a count of the number of times the keyword occurs in the document.
The IHttpHandler
interface contains two members that must be implemented in our custom handler class: the ProcessRequest
method and the Reuseable
property. As shown in Listing 17-14, ProcessRequest
contains the code that processes the HTTP request; IsReusable
indicates whether the instance of the handler can be reused for other requests. This is applicable only when a handler factory is providing handlers and pools them for reuse.
This handler responds to URL requests that contain a text file and query string containing a keyword to search for in the file:
HTTP://www.scilibrary.com/films.txt?kwd=Bogart
The context
object provides access to the file name through the Request.PhysicalPath
property. An attempt is made to open the text file and read it line by line. Each line is searched for the keyword using Regex.Match
. If a match is found, the method BoldWord
is called to place HTML bold (<b>
) tags around the text. This method also increments the word counter.
Example 17-14. Custom HTTP Handler to Display a Text File
// file: txthandler.cs using System; using System.Web; using System.IO; using System.Text.RegularExpressions; public class TextHandler: IHttpHandler { static int wordCt=0; static string BoldWord(Match m) { // Get the matched string and place bold tags around it string kwd = m.ToString(); kwd="<b>"+kwd+"</b>"; wordCt += 1; return kwd; } public void ProcessRequest(HttpContext ctx) { // Get file to be opened string filePath= ctx.Request.PhysicalPath; int ndx= filePath.LastIndexOf(@""); string fileName = filePath.Substring(ndx+1); // Keyword to search for in file string keyWord = ctx.Request["kwd"]; // Create HTML response ctx.Response.Output.Write("<html><body >"); ctx.Response.Output.Write("File: "+fileName+ " Keyword: <b>"+keyWord+"</b><hr size=1>"); string line; try { StreamReader reader= new StreamReader(filePath); // Read lines of file and display in response while ((line = reader.ReadLine()) != null) { if (keyWord!= null) { // search for keyword and highlight string newLine = Regex.Replace(line, keyWord, new MatchEvaluator(TextHandler.BoldWord), RegexOptions.IgnoreCase); line= newLine; } ctx.Response.Output.Write("<br>"+line); } reader.Close(); } catch (Exception e) { ctx.Response.Output.Write("<br>"+e.Message); } // Display number of matches ctx.Response.Output.Write("<br><br>Word Matches: " + wordCt.ToString()); ctx.Response.Output.Write("</body></html>"); wordCt=0; // reset since it is static } public bool IsReusable { get {return false;} } }
The final code is compiled and placed in the in
subdirectory of the application.
For ASP.NET to be aware of the handler, an entry is made in the machine.config
or application's web.config
file. In this example, we place an <add>
element in the web.config
file. This element contains three attributes that define the handler and the conditions for using it. Verb
specifies the type of request to be handled, such as GET
or POST
. The *
is a wildcard character that represents any type request. The path
attribute indicates the requested resource name that is mapped to the handler; and type
specifies the handler class or handler factory and its containing assembly.
<httpHandlers> <add verb="*" path="*.txt" type="TextHandler,txthandler"/> </httpHandlers>
The final step in deployment is to make IIS (Internet Information Service) aware that requests for the .txt
extension are to be handled by ASP.NET. This is done by using Internet Services Manager to map the file extension to the ISAPI extension DLL (aspnet_isapi.dll
). Follow these steps:
Invoke Internet Services Manager and right-click Default Web Site.
Select Properties – Home Directory – Configuration.
Click Add and a pop-up window appears.
Fill in the Executable field with the path to the aspnet_isapi.dll
file. This file should be in the version directory of the Framework installation.
Fill in the Extension field with *.txt.
With these steps completed, TextHandler
is now called to process all requests for .txt
files. Figure 17-12 shows output from a sample request.
The ASP.NET environment offers a variety of types and configuration files that enable a developer or software architect to customize how Web requests are handled by a server. Configuration files such as web.config
provide a flexible and easy way to customize the ASP.NET settings. These files contain sections that enable/disable tracing, specify the default culture of a page, define the way session state information is stored, and define an authentication/authorization security model. The security model enables an application to limit page access to users based on credentials such as their name, password, and role. Credential information may be stored in the web.config
file, an XML file, or an application's proprietary database.
ASP.NET provides several mechanisms for handling both Application and Session state data. The application data is available to all sessions using that application; session data is limited to the individual session and can be configured to support Web farms and permanent storage in a database. An alternative to using an application state object is data caching. ASP.NET provides an area in memory where CacheEntry
objects are stored and made available to all sessions. Output caching is also supported to enable an entire Web page or Web page fragment to be stored in a cache on the server, a proxy server, or the browser. This reduces the load on a server by allowing a Web page to be retrieved from memory rather than being recreated on the server.
Advanced Web application development can benefit from an understanding of the underlying ASP.NET architecture that supports HTTP communications. Central to this is the HTTP pipeline through which requests and responses are transported. After a request or response enters this pipeline, it can be parsed, modified, rerouted, or rejected. Both the global.asax
file and HTTP module can be designed to respond to pipeline events. The event handlers they supply are used to do such things as append standard information to all responses, maintain Web statistics, and transparently reroute requests when necessary. At the end of the pipeline, a handler is responsible for providing the requested resource. Custom handlers can be written to handle specially defined resources or enhance the existing handlers.