ASP.NET Page Persistence Using Extended Attributes
David Hovel
Version 1.2
July 20, 2002
Email: davidhov@exmsft.com
Abstract:
The stateless nature of the HTTP protocol requires that web developers employ mechanisms outside the scope of their implementation language to maintain program state associated with a particular HTTP session. This article shows how the Microsoft .NET family language feature of extended attributes can be used with the .NET reflection capability to formalize HTTP state management in a declarative manner.
The HTTP protocol has succeeded as well as it has in large measure due to its simplicity. It uses text messaging, layers on top of TCP, and is stateless. The statelessness of HTTP, while streamlining web server operations, presents difficulties to web site builders developing active content.
The stateless of HTTP means that there is no formal means of remembering, either in the client or in the server, any past interaction involving the same user. Every web development tool ensemble provides some means of recognizing sessions and storing information associated with a particular session between HTTP requests.
In building a reusable solution for this problem for ASP.NET, I exploited the reflection capability of the .NET languages. Reflection allows detailed, run-time examination of the contents of classes, and appears to provide opportunities for meta-reasoning in imperative or procedural programs that have generally been available only in symbolic languages such as Prolog, LISP and Scheme.
Microsoft’s recently introduced .NET language family includes a new generation of its Active Server Pages (ASP) technology called ASP.NET. Running under the control of Microsoft’s Internet Information Server, the standard classes of ASP.NET provide several distinct mechanisms for storing information about the client and the client’s session. There are clear tradeoffs between these mechanisms, but all of them have one thing in common: each requires special coding by the application developer on each page.
ASP.NET allows ASPX pages to access information from the following collections:
In addition, important configuration information can be added to a special XML file called web.config. Data entered in this file is loaded every time the application starts.
ASP.NET can create the illusion of session orientation in several ways. Most commonly it stores a cookie on the client’s machine that contains a unique session identifier. When the client makes a new request, the cookie’s information is encoded into the new POST request and passed to the server. ASP.NET uses this information to index into a collection of session state information.
Each of these state storage mechanisms must be used wisely, and each must be used explicitly by invocation in the “code-behind” module for a given web page. So, what is a “code-behind” module?
In traditional ASP, a single text file with the extension of ASP contained raw HTML interspersed with server-side and client-side scripts in VBScript or JScript (Microsoft’s version of ECMAScript). While convenient, such intermixture quickly led to serious coding anarchy.
The Microsoft .NET team decided to segregate, insofar as possible, the HTML presentation code and client-side scripts from the server-side scripting code. In ASP.NET, the typical ASPX page is represented by two files: the ASPX file itself, containing the invariant HTML to be rendered and a second “code-behind” file, typically written in C# or Visual Basic.NET, containing the procedural code to generate the dynamic HTML tags. An extensive data and eventing model allows interaction between the ASPX file and the code-behind file.
A code-behind file defines a .NET web page handler class that encapsulates the server-side rendering logic for the page. When an HTTP request is received for a particular ASPX page, the ASP.NET mechanism automatically creates a freshly minted instance of the page class declared in the code-behind module. Very convenient—except that this page instance has no history.
Since ASP.NET supports object-oriented representations of web pages, I found that the first thing I wanted to do with ASP.NET was formalize the handling of session state. My older ASP code was sprinkled with references to the Application and Session collections. Incorrectly handling any one of these would have brought the application down quickly.
I realized that the ideal solution would be object-oriented: the code-behind page instance should automatically contain the exact same values in its data field members that it did the last time such an instance existed for the same session. In other words, I wanted to be able to “pretend” that the same instance of the page class had lain dormant just waiting for the next request to come down the fiber from the same client session. This would allow me to employ the same coding patterns that I’d used for many years.
Assuming I could implement such a declarative solution, how should it work? Again, there are five repositories of useful information: Cookies, the ViewState collection, the Session collection, the Application collection and the web.config file.
For my purposes, I decided that I didn’t want to use cookies or the ViewState collection for automatic persistence. Both have similar constraints. Not all types of objects can be stored in them, since cookies store only textual information and the ViewState is limited to objects that can be serialized (converted back and forth to text). Also, these mechanisms consume bandwidth, since such information is transmitted repeatedly between the server and the client.
In the end, these usage rules seemed reasonable:
How could this work? Ideally, again, I wanted to just mark a field as “please persist” and have it work magically. Considering this, I remembered a similar problem in object serialization under the .NET Framework. When class objects are serialized for storage or transmission, there are always fields that shouldn’t or needn’t be handled in this way. The .NET team decided to decorate these fields with additional information that could be examined by serialization code at run-time. If marked as “don’t serialize”, the field would be ignored, saving time and bandwidth.
They accomplish this feat by exploiting what are called extended attributes. In .NET languages, classes and their elements can be marked with non-procedural declarations in square braces that are parsed and validated by the compiler and stored as part of the type information in the “assembly”, the executable file that results from the compilation. This means that such information is always available for inspection at run-time.
(Note: all examples in this article are in the C# language.)
An extended attribute is declared like a standard class. The following example declares an enumeration and an attribute class called PagePersist that contains a value from the enumeration.
public enum PagePersistEnum {
ePersistSession,
ePersistApplication,
ePersistWebConfig };
[AttributeUsage(AttributeTargets.Field)]
public class PagePersistAttribute : System.Attribute
{
public PagePersistAttribute ( PagePersistEnum ePersist )
{
this.ePersist = ePersist;
}
public PagePersistEnum PersistLevel
{
get { return ePersist; }
}
protected PagePersistEnum ePersist;
}
The AttributeUsage declaration specifies that the PagePersist attribute is applicable only to fields, the data members of a class. To use the PagePersist attribute, a field in a normal class is prefaced by an attribute declaration.
class Demo
{
public Demo() {}
[PagePersist(PagePersistEnum.ePersistSession)]
protected string sTest = “this is the test string”;
}
The C# compiler kindly verifies that the attribute is correctly constructed, and then stores the attribute information along with the complete class type information in the resulting assembly.
It is very simple to examine the components of a class (fields, methods, enumerations, etc.) at run-time and take action accordingly. The .NET Framework calls this sort of behavior “reflection”; it is similar to Java’s “introspection” mechanism. This capability applies to extended attributes also.
The next step in my persistence solution was to develop a class that could examine another class’ type information, locate the fields to be persisted and take the correct action.
In the accompanying source code, the file PersistAttr.cs defines a class called the PagePersister that uses reflection to find the fields within a class marked with persistence attributes and either restore or save their values using the appropriate persistence mechanism.
Its primary method of the PagePersister class is called Action. It saves values from or loads values into static or instance fields of a particular class.
In pseudo-code, it runs something like this.
Select the kind of members to be examined (protected static or protected instance members) and get a collection of all the members of that kind.
For each such member
{
If the member is a field (data variable)
{
Get a collection of the extended attributes on the field.
If one of them is a page persistence attribute
{
If session persistence, use the page’s Session
collection, else
If application persistence, use the global
Application object, else
Use web.config file information through the
ConfigurationSettings collection (read only).
}
}
}
Field data is stored into the Session and Application collections using string keys with a special prefix and the page’s class name. This helps identify persisted variables and avoids collisions between similarly named values in different pages.
As objects are recovered from the persistent collections, their references are removed so that the ASPX code page may recreate or dispose of them as required.
Once the fields of a code-behind page class have been decorated with the persistence attribute, all that remains is for its values to be restored when a new instance is created and stored when the instance is discarded. Remember that a new page instance will be created for each incoming request. The active lifetime of the page is bounded by two events: Page_Load and Page_Unload.
In Visual Studio.NET, the creation of a new web project in C# generates a module called Global.asax.cs that represents the HTTP application. ASP.NET provides a pair of events for this generated Global module that are useful for centralizing persistence handling: PreRequestHandlerExecute and PostRequestHandlerExecute. During the pre-request event, the new page object has been created, but its Page_Load function has not been yet called. When the post-request event occurs, Page_Unload has just been called.
In other words, these events scope the active lifetime of the page and hence are ideal for the logic required. In the sample, pre-request and post-request event functions in Global call the following helper function in PersistAttr.cs:
public static void PersistPage(HttpApplication app, bool bRestore)
{
// Access the page object that will handle this request.
System.Web.UI.Page page = app.Context.Handler
as System.Web.UI.Page;
if ( page == null )
return; // We only handle page classes
// Create a persister helper for operating on this page
PagePersister persist = new PagePersister(page);
string sMethod = app.Context.Request.HttpMethod.Trim().ToUpper();
bool bInitialRequest = app.Session.IsNewSession
|| sMethod != "POST";
if ( bRestore )
{
if ( ! bInitialRequest )
persist.Restore();
}
else
persist.Save();
}
The PersistPage function accesses the current handler defined for this request in the HttpApplication Context object. It checks to see of the handler is a subclass of System.Web.UI.Page, the standard superclass for ASPX pages. If not, it returns. Then it creates a PagePersister object. If the page is being loaded and it’s not an initial request, its fields are restored; otherwise, its fields are saved.
Each ASPX page class has a property called IsPostBack, but it does not appear to be set before PreRequestHandlerExecute is called, so this routine checks other variables to see if the request is in fact a POST reply.
In the following code fragment, an ASPX page class is declared with a single instance variable—an array of strings.
public class PersistPage : System.Web.UI.Page
{
private void Page_Load(object sender, System.EventArgs e)
{
if ( ! IsPostBack )
{
// Initialize all variables to desired original state
string[] sa = {"First","Second","Third","Fourth"};
arrayString = sa;
}
}
[PagePersist(PagePersistEnum.ePersistSession)]
protected string[] arrayString;
}
Note that the field arrayString is prefaced by a PagePersist attribute. Any number of fields in a page may be decorated in the manner.
The result is that between the execution of Page_Load and Page_Unload, the developer may treat the C# class as though it were part of any other type of application; that is, stateful: previous changes to its fields are present, new changes will be remembered. If it later appears that previously unsaved variables must now be persisted, only the persistence attribute needs to be added.
Since the static fields of a class may be initialized in C#, I decided to mimic this behavior by providing the option of a secondary static initialization for classes in the ASP.NET application’s Global module. The Global module receives an Application_Start event when it’s loaded for the first time.
protected void Application_Start(Object sender, EventArgs e)
{
// Restore the static variables of the Global
class and the
// PersistPage class.
PagePersister.RestoreStatic(Application,typeof(Global));
// Restore the static variables of the all
subclasses of the
// System.Web.UI.Page class.
PagePersister.RestorePageStatics(Application);
}
In this function, the call to RestoreStatic restores the static fields of the Global object itself from the web.config file. Then a call to RestorePageStatics restores the static variables of the code-behind classes of all ASPX pages used in the application.
How does RestorePageStatics know the identities of all the page classes in the application? The .NET reflection capability allows me to iterate over all classes in the application and discover those classes that are immediate descents of System.Web.UI.Page.
public static void RestorePageStatics(HttpApplicationState app)
{
Assembly asmb = Assembly.GetExecutingAssembly();
Type[] types = asmb.GetExportedTypes();
Type tPage = typeof(System.Web.UI.Page);
foreach ( Type t in types )
{
if ( tPage == t.BaseType )
RestoreStatic(app, t);
}
}
This routine demonstrates how straightforward type analysis can be in .NET. First, the routine gets a collection of all the types exported by the executing assembly. Then it iterates over the types, looking for those that are direct descendents of System.Web.UI.Page, the base class for ASP.NET pages. The static fields of each such class are restored.
Creating a standard procedure for handling persistence information also simplifies end-of-session logic. When this happens, usually due to a time-out, the Global module receives the Session_End event.
protected void Session_End(Object sender, EventArgs e)
{
PagePersister.SessionEnd(Session);
}
The PagePersister’s SessionEnd function searches the Session collection. All persisted objects that support the IDisposable interface are correctly disposed. (Typically, objects supporting IDisposable have precious system resources that should be released immediately without waiting for garbage collection.)
The page persistence class in effect creates, through the use of standard facilities, what could almost be considered a new language feature and its corresponding mechanics. All of the features is uses are fully supported because they are integral to the .NET Framework.
One reason that I tackled such a project while still a novice to the .NET paradigm was that I wanted to “kick the tires” of the reflection capability. As an old Prolog fan, I always wanted the ability in, say, C++, to examine classes at run-time in order to pretty-print objects or create smart “deep copy” object cloning routines.
As the sample code shows, reflection is very easy to use. In particular, there are two areas that were simpler than I anticipated. The first concerned assigning values, since assigning arbitrary objects to typed fields would normally require casting. However, the SetValue method of the FieldInfo class handled all such assignment easily. Exceptions are thrown if the values are incompatible, but the type coercion was transparent. In addition, the “boxing” capability of the .NET languages meant that I was not concerned if the variable was a struct (handled by value) or a class (handled by reference).
The second area that was intriguing was the ability to examine the assembly as a whole. As stated above, I used this feature to discover all the code-behind ASPX page classes declared in the assembly that contained my sample web application.
Some other obvious uses for type analysis come to mind, such as intelligent dump analyzers, automated style checkers, parser generators and functional programming. .NET allows classes to be created dynamically (“emitted”) and loaded dynamically. Coupled with reflection, many types of data-driven and declarative programming common in the LISP and Prolog communities are well within the reach of the .NET Framework. Unlike the older symbolic languages, however, the .NET languages provide additional benefits such as speed, safety and portability.