- Overview of ASP.NET Applications
- Using Application State
- Using the Web.Config File
- Using HTTP Handlers and Modules
- Summary
Using Application State
You can store variables and objects directly in application state. An item that has been added to application state is available within any ASP.NET page executing in the application. Items stored in application state function like global variables for an application.
NOTE
Application state is represented by the HttpApplicationState class.
The following statement, for example, creates a new item named myItem with the value Hello! in application state:
Application( "myItem" ) = "Hello!"
After this statement is executed in an ASP.NET page, the value of myItem can be retrieved within any other ASP.NET page contained in the same application. To read the value of an application variable, you can use a statement like this:
Response.Write( Application( "myItem" ) )
This statement displays the value of the application item named myItem.
You can add more complex objects, such as collections and DataSets, in an application state. (You should read the warnings in the next section before doing so, however.) For example, you can add an existing DataSet to application state like this:
Application( "DataSet" ) = dstDataSet
After you add the DataSet with this statement, you can retrieve it from application state within any ASP.NET page and display its contents.
Classic ASP
In previous versions of Active Server Pages, the Application object was frequently used to cache data. You can still use application state for this purpose. However, the ASP.NET framework includes a new object, named the Cache object, that provides you with a richer set of methods and properties for working with cached data. To learn more details about caching data, see Chapter 17, "Caching ASP.NET Applications."
You can remove an item from application state by using the Remove method like this:
Application.Remove( "myItem" )
If you want to remove all items from application state, you can use either the Clear or RemoveAll method. For example, you would use the RemoveAll method as follows:
Application.RemoveAll()
After an item is added to application state, it remains there until the application is shut down, the Global.asax file is modified, or the item is explicitly removed. So, be cautious about storing too much data in application state.
CAUTION
You can configure ASP.NET so that it automatically shuts down the currently running application and replaces it with a new one on a timed basis. (Shutting down an application on a timed basis can make your site more stable.) When the application shuts down, you lose all data stored in application state.
Understanding Application State and Synchronization
When you add items to application state, the items can be accessed within multiple pages at the same time. This fact can result in conflicts and problems. Consider the sample page in Listing 15.1.
Listing 15.1 BadPageCounter.aspx
<Script Runat="Server"> Sub Page_Load Application( "PageCounter" ) += 1 lblCount.Text = Application( "pageCounter" ) End Sub </Script> <html> <head><title>BadPageCounter.aspx</title></head> <body> This page has been requested: <asp:Label ID="lblCount" Runat="Server" /> times! </body> </html>
The page in Listing 15.1 uses an item stored in application state to create a simple page counter. Every time the page is requested, the application item named PageCounter is incremented by one (see Figure 15.1).
Figure 15.1 Simple page view counter.
This page has one important problem, however. Suppose that the current value of PageCounter is 590. Now imagine that two people, named Fred and Jane, request the page at the same time. When both Fred and Jane request the page, PageCounter has the value 590. When Fred's request for the page finishes processing, the value of PageCounter is incremented by one so that it has the new value 591. When Jane's request for the page finishes processing, the value of PageCounter is incremented by one so that it has the new value 591. See the problem?
Because items stored in application state are shared among multiple pages, conflicts can occur. In this simple case, the worst outcome is inaccurate data. However, when users are working with more complicated objects in application statesuch as collectionsconcurrent access to items stored in application state can result in deadlocks, race conditions, and access violations. In other words, storing items in application state can cause your application to crash in a very messy manner.
Fortunately, you can find ways to get around this problem. You can force access to items in application state to take place in an orderly manner by using two methods of the application state object: Lock and Unlock.
The Lock method locks the application state object so that it can be accessed only by the current thread. Typically, this means that only the current page can access objects in application state.
The Unlock method releases the lock on application state. It enables other pages to access values in application state again.
NOTE
You should always unlock the application state object as quickly as possible. However, if you do forget to call Unlock after Lock, the lock is automatically released when the current request finishes processing, an error occurs, or the request times out.
The page in Listing 15.2 illustrates how you can use the Lock and Unlock methods.
Listing 15.2 GoodPageCounter.aspx
<Script Runat="Server"> Sub Page_Load Application.Lock Application( "PageCounter" ) += 1 lblCount.Text = Application( "pageCounter" ) Application.Unlock End Sub </Script> <html> <head><title>GoodPageCounter.aspx</title></head> <body> This page has been requested: <asp:Label ID="lblCount" Runat="Server" /> times! </body> </html>
In Listing 15.2, the Lock method is called immediately before PageCounter is updated. The Unlock method is called immediately after PageCounter is assigned to the Label control.
Notice that the Lock method locks the application state object as a whole. You cannot selectively lock items in application state. Because locking application state blocks all other access to any item, using application state unwisely in a high volume Web site can have a serious impact on the performance of the site.
You must be cautious about using complex objects, such as collections, in application state. Collections such as ArrayLists and HashTables are not designed to be accessed by multiple threads at the same time. This means that you should either use the Lock and Unlock methods whenever accessing collections in application state, or you should create thread-safe versions of the collections.
You can create thread-safe versions of the collection objects by using the Synchronized() method, which returns a thread-safe wrapper for the collection. For example, the following statements return a thread-safe instance of an ArrayList:
Dim colArrayList As ArrayList colArrayList = New ArrayList colArrayList.Add( "Plato" ) colArrayList.Add( "Frege" ) colArrayList.Add( "Carnap" ) colArrayList = ArrayList.Synchronized( colArrayList )
In this example, a new ArrayList containing three items is created. When first created, the ArrayList is not thread-safe. However, in the final statement, the Synchronized method is used to return a thread-safe wrapper for the ArrayList.
Using the Global.asax File
Every ASP.NET application can contain a single file named Global.asax in its root directory. This special file can be used to handle application wide events and declare application wide objects.
WARNING
The Global.asax file is not used to display content. If you request this file, you receive an error. This error is intentional. Enabling users to view the contents of the Global.asax file would constitute a serious security hole.
Every ASP.NET application supports a certain number of events. The following is a list of the most important of these events:
Application_AuthenticateRequestRaised before authenticating a user.
Application_AuthorizeRequestRaised before authorizing a user.
Application_BeginRequestRaised by every request to the server.
Application_EndRaised immediately before the end of all application instances.
Application_EndRequestRaised at the end of every request to the server.
Application_ErrorRaised by an unhandled error in the application.
Application_PreSendRequestContentRaised before sending content to the browser.
Application_PreSendRequestHeadersRaised before sending headers to the browser.
Application_StartRaised immediately after the first application is created. This event is guaranteed to occur only once.
DisposeRaised immediately before the end of a single application instance.
InitRaised immediately after each application instance is created. This event might occur multiple times.
This list contains the standard application events. If you add other modules to your application, additional events are exposed in the Global.asax file. For example, FormsAuthenticationModule exposes a Forms_Authenticate event, and SessionStateModule exposes both Session_Start and Session_End events.
In the section titled "Using HTTP Handlers and Modules" later in this chapter, you learn how to add your own modules to the Global.asax file.
Classic ASP
The Global.asax file is backward-compatible with the Global.asa file. To retain compatibility with the Global.asa file, you can refer to events by using the syntax Application_OnEventName. For example, you can use either Application_Start or Application_OnStart in the Global.asax file.
You can handle any of these events in the Global.asax file by adding the appropriate subroutine. In general, the subroutine should look like this:
Sub Application_EventName ... application code End Sub
Modifying the Global.asax file restarts the application. Any information stored in application (or session) state is lost. So, be cautious about modifying the Global.asax file on a production Web site.
Understanding Context and Using the Global.asax File
Within an ASP.NET page, the Page object is the default object. In many cases, if you do not specify an object when you call a method or access a property, you are implicitly calling the method or accessing the property from the Page object. For example, when you call the MapPath method (which maps virtual paths to physical paths), you are actually calling the Page.MapPath method. Or, when you access the Cache property, you are implicitly accessing the Page.Cache property.
The Global.asax file does not have the Page object as its default object. This means that you cannot simply call a method such as MapPath and expect it to work. Fortunately, in most cases, you can use a simple workaround. Instead of calling the MapPath method, you can call the Context.MapPath method. Or, instead of accessing the Page.Cache object, you can access the Context.Cache object.
If a familiar property or method does not work in the Global.asax file, you should immediately try calling the method or property by using the Context object instead.
Handling the Application Start and Init Events
Imagine that you have just plugged your Web server into a power outlet and the Web server has booted up. When the first user makes a request to your Web site, the Application_Start event occurs.
The Application_Start event is guaranteed to occur only once throughout the lifetime of the application. It's a good place to initialize global variables. For example, you might want to retrieve a list of products from a database table and place the list in application state or the Cache object.
NOTE
To learn more details about the Cache object, see Chapter 17, "Caching ASP.NET Applications."
The page in Listing 15.3 illustrates how you can add the contents of the Products database table to the Cache object within a subroutine that handles the Application_Start event.
Listing 15.3 AppCache/Global.asax
<%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <Script Runat="Server"> Sub Application_Start Dim conNorthwind As SqlConnection Dim strSelect As String Dim dadProducts As SqlDataAdapter Dim dstProducts As DataSet conNorthwind = New SqlConnection( "Server=localhost;UID=sa;PWD=secret;Database=Northwind" ) strSelect = "Select * From Products" dadProducts = New SqlDataAdapter( strSelect, conNorthwind ) dstProducts = New DataSet() dadProducts.Fill( dstProducts, "Products" ) Context.Cache( "Products" ) = dstProducts End Sub </Script>
NOTE
Within this Global.asax file, you must use Context.Cache, rather than Cache, because the Page object is no longer the default object.
You can find the Global.asax file in Listing 15.3 in the AppCache subdirectory on the CD. To test how the page works, you can open the DisplayProducts.aspx page, which displays the list of products added to the Cache object (see Figure 15.2).
Figure 15.2 Displaying Cached products.
The ASP.NET framework uses a pool of application instances to process each request to the server. When a request is made, an application instance is assigned to the request. Immediately after any of these application instances are created, the Init event is raised.
Requesting the first page from the Web site raises both the Application_Start and Init events. The Application_Start event won't be raised again for the lifetime of the application. The Init event, on the other hand, might be raised multiple times.
You can use the Init event to initialize any variables or objects that you'll need to use throughout the lifetime of a particular application instance. If you assign values to local variables, the variables retain their values across multiple requests.
The page in Listing 15.4, for example, illustrates how you can automatically add a random banner advertisement to the bottom of every page (see Figure 15.3).
Figure 15.3 Displaying random banner advertisement.
Listing 15.4 AppInit/Global.asax
<%@ Import Namespace="System.Data" %> <Script Runat="Server"> Dim dtblBannerAds As DataTable Overrides Sub Init() Dim dstBannerAds As DataSet dstBannerAds = New DataSet dstBannerAds.ReadXml( Server.MapPath( "adFile.xml" ) ) dtblBannerAds = dstBannerAds.Tables( 0 ) End Sub Sub Application_PreSendRequestContent Dim strPageFooter As String Dim objRan As Random Dim drowSelectedAd As DataRow objRan = New Random() drowSelectedAd = dtblBannerAds.Rows( _ objRan.Next( dtblBannerAds.Rows.Count ) ) strPageFooter = "<hr><a href=" & drowSelectedAd( "NavigateUrl" ) strPageFooter &= "><img src=" & drowSelectedAd( "ImageUrl" ) strPageFooter &= "></a>" Response.Write( strPageFooter ) End Sub </Script>
You can find the Global.asax file in Listing 15.4 in the AppInit subdirectory on the CD that accompanies this book. You can open the page named DisplayAd.aspx to see how a banner advertisement is automatically appended to every page.
In the Init subroutine in Listing 15.4, a list of banner advertisements is loaded from an XML file named adFile.xml into a DataSet. This subroutine is executed only once for each application instance.
Next, within the Application_PreSendRequestContent subroutine, one advertisement is randomly selected from the DataSet and sent to the browser. The PreSendRequestContent subroutine is executed just before content being sent to the browser. Therefore, outputting content within this subroutine guarantees that it will be displayed at the bottom of the page.
Notice that initializing variables in the Init subroutine is similar to adding items to application state. The crucial difference is that items added to application state are guaranteed to survive across multiple application instances, whereas variables created in the Init subroutine survive only one application instance.
Handling the Application_BeginRequest Event
The Application_BeginRequest event is raised when the request starts to be processed. You can exploit this event in several ways. Suppose, for example, that you want to create vanity URLs at your Web site. You want registered users to be able to enter a URL, such as http://www.superexpert.com/swalther, and view a page customized only for them.
You would not, however, want to manually create a new page for each user. Instead, you would want to secretly transfer the user to a single page that handles all user requests for the page. In that case, you can capture the Application_BeginRequest event and automatically transfer the user to the new page by using the RewritePath method. The Global.asax page in Listing 15.5 demonstrates how you can use the RewritePath method with the Application_BeginRequest event. (You can find this file in the AppVanity subdirectory on the CD that accompanies this book.)
Listing 15.5 AppVanity/Global.asax
<Script Runat="Server"> Sub Application_BeginRequest Dim strUsername Dim strCustomPath As String If INSTR( Request.Path.ToLower, "custom" ) = 0 Then strUsername = Request.Path.ToLower strUsername = strUsername.Replace( ".aspx", "" ) strUsername = strUsername.Remove( _ 0, _ InstrRev( strUsername, "/" ) ) If Request.ApplicationPath = "/" Then strCustomPath = _ String.Format( _ "/custom/default.aspx?username={0}", _ strUsername ) Else strCustomPath = _ String.Format( _ "{0}/custom/default.aspx?username={1}", _ Request.ApplicationPath, _ strUsername ) End If Context.RewritePath( strCustomPath ) End If End Sub </Script>
The Global.asax file in Listing 15.5 contains an Application_BeginRequest subroutine. This subroutine checks whether the current URL contains the name of the custom subdirectory in its path. If not, the beginning and end of the URL are stripped, and the RewritePath method transfers the user to the /custom/default.aspx page.
If you enter the URL http://superexpert.com/swalther.aspx in the browser address bar, it is automatically rewritten like this:
http://superexpert.com/custom/default.aspx?username=swalther
Within the Default.aspx file in the custom subdirectory, you can grab the username query string and customize the page for the particular user. The page in Listing 15.6 simply displays the username in the body of the page. (You can find this page in the /AppVanity/Custom subdirectory on the CD that accompanies this book.)
Listing 15.6 AppVanity/Custom/Default.aspx
<Script Runat="Server"> Dim strUsername As String Sub Page_Load strUsername = Request.Params( "username" ) End Sub </Script> <html> <head><title>Welcome!</title></head> <body> Hello <b><%=strUsername%></b> and welcome to your Web site! </body> </html>
The page in Listing 15.6 retrieves the username parameter from the query string and displays it (see Figure 15.4). A more complicated page might grab data from the database associated with the particular user and display the information.
Figure 15.4 A vanity page.
You also could use the RewritePath method with the Application_BeginRequest event to fix broken links. For example, you could create an XML file that maps bad links to good links. Say that your Web site included products and services pages in the past, but now you have removed them and you want to redirect all users to the default page. The XML file in Listing 15.7 illustrates how you could create this XML file. (You can find this file in the AppFixLinks subdirectory on the CD that accompanies this book.)
Listing 15.7 AppFixLinks/BadLinks.xml
<badlinks> <link> <badlink>/products.aspx</badlink> <goodlink>/default.aspx</goodlink> </link> <link> <badlink>/services.aspx</badlink> <goodlink>/default.aspx</goodlink> </link> </badlinks>
The XML file in Listing 15.7 maps the URLs /products.aspx and /services.aspx to the URL /default.aspx.
The Global.asax file in Listing 15.8 uses the RewritePath() method to automatically redirect users from bad links to good links.
Listing 15.8 AppFixLinks/Global.asax
<%@ Import Namespace="System.Data" %> <Script Runat="Server"> Sub Application_BeginRequest Dim dtblBadLinks As DataTable Dim strThisUrl As String Dim strSelect As String Dim arrMatches() As DataRow Dim strGoodLink As String dtblBadLinks = GetBadLinks() strThisUrl = Request.Path.ToLower() If Request.ApplicationPath <> "/" Then strThisUrl = strThisUrl.Remove( 0, Request.ApplicationPath.Length ) End If strSelect = "badlink='" & strThisURL & "'" arrMatches = dtblBadLinks.Select( strSelect, "badlink" ) If arrMatches.Length > 0 Then strGoodLink = arrMatches( 0 )( "goodlink" ) strGoodLink = Request.ApplicationPath & strGoodLink Context.RewritePath( strGoodLink ) End If End Sub Function GetBadLinks() As DataTable Dim dstBadLinks As DataSet Dim dtblBadLinks As DataTable dtblBadLinks = Context.Cache( "badlinks" ) If dtblBadLinks Is Nothing Then dstBadLinks = New DataSet dstBadLinks.ReadXml( Server.MapPath( "badlinks.xml" ) ) dtblBadLinks = dstBadLinks.Tables( 0 ) Context.Cache.Insert( "badlinks", _ dtblBadLinks, _ New CacheDependency( Server.MapPath( "badlinks.xml" ) ) ) End If Return dtblBadLinks End Function </Script>
The Badlinks.xml file is retrieved in the GetBadLinks() function. If the Badlinks.xml file is not already cached in memory, it is added to the Cache object. (The Badlinks.xml file is inserted with a Cache file dependency so that the Cache object is automatically updated if the Badlinks.xml file is modified.)
NOTE
To learn more about creating file dependencies see Chapter 17, "Caching ASP.NET Applications."
In the Application_BeginRequest subroutine, the path of the current URL is checked against the list of bad links contained in the Badlinks.xml file. If a match is made, the RewritePath() method automatically transfers the user from the bad link to the good link.
If a user enters the address http://superexpert.com/products.aspx in a Web browser, he or she is automatically transferred to the following new page specified in the Badlinks.xml file:
http://superexpert.com/default.aspx
You can use this method to fix any bad links on your Web site. All you need to do is maintain the Badlinks.xml file.