WHAT’S IN THIS CHAPTER?
WROX.COM CODE DOWNLOADS FOR THIS CHAPTER
Please note that all the code examples in this chapter are available as a part of this chapter’s code download on the book’s website at www.wrox.com.
HTTP is a stateless protocol, so it cannot remember things. Many current web applications have to maintain state, though: They need to remember the logged-in user, the contents of the shopping cart, and more.
Before the web became popular, standard client-server architecture meant using a fat client and a fat server. Perhaps your desktop application could talk to a database. The state was held either on the client-side in memory, or in the server-side database. Typically, you could count on a client having a little bit of memory and a hard drive of its own to manage state. The most important aspect of traditional client/server design, however, was that the client was always connected to the server. It’s easy to forget, but HTTP is a stateless protocol. For the most part, a connection is built up and torn down each time a call is made to a remote server. Yes, HTTP 1.1 includes a keep-alive technique that provides optimizations at the TCP level. Even with these optimizations, the server has no way to determine that subsequent connections came from the same client.
ASP.NET’s session management support provides an easy API to store data during a session; the whole “heavy lifting” is done by the framework, so we do not have to worry about it. But there is more — this chapter will present several state management techniques so that you can pick the one that suits your needs best.
Given a stateless protocol such as HTTP, and ASP.NET on the server side, how do you manage state on the web? Figure 21-1 is a generalized diagram that identifies the primary means available for managing state. The problem is huge, and the solution range is even larger. This chapter assumes that you are not using Java applets or ActiveX controls to manage state, because these techniques do not work on all systems.
If you remember one thing about state management, remember this: There is no right answer. Some answers are more right than others, certainly; but many, many ways exist for managing state. Think about your last project. How many days did you spend trying to decide where you should manage state? The trick is to truly understand the pros and cons of each method.
To make an educated decision about a method, you should understand the lifecycle of a request and the opportunities for state management at each point in the process:
One of the repeating themes you might notice is the agreement between the client and the server to pass information back and forth. That information can be in the URL, in HTTP headers, or even in the submitted form as an input field.
On the server side, you have a few options. You will want to weigh the options based on the amount of storage you have available, the amount of data you want to store, and how often and how fast you will require access to the data.
Tables 21-1 and 21-2 express each of the server-side and client-side options and list a few pros and cons for each.
SERVER-SIDE OPTION | PROS | CONS |
Application State | Fast. Shared among all users. | State is stored once per server. |
Cache Object (Application Scope) | Like the Application State but includes expiration via dependencies (see Chapter 22 on caching). | State is stored once per server in multiple server configurations. |
Session State | Three choices: in process, out of process, and DB-backed. Can be configured as cookieless. | Can be abused. You pay a serialization cost when objects leave the process. In process requires web server affinity. Cookieless configuration makes hijacking easier. |
Custom Database | State can be accessed by any server in a web farm. | Pay a serialization and persistence cost when objects leave the process. |
CLIENT-SIDE OPTION | PROS | CONS |
Cookie | Simple | Can be rejected by browser. Not appropriate for large amounts of data. Inappropriate for sensitive data. Size cost is paid on every HTTP request and response. |
Hidden Field | Simple for page-scoped data | Not appropriate for large amounts of data. Inappropriate for sensitive data. |
ViewState | Simple for page-scoped data | Encoding of serialized object as binary Base64-encoded data adds approximately 30 percent overhead. Small serialization cost. Has a negative reputation, particularly with DataGrids. |
ControlState | Simple for page-scoped control-specific data | Like ViewState, but used for controls that require ViewState even if the developer has turned it off. |
QueryString (URL) | Incredibly simple and often convenient if you want your URLs to be modified directly by the end user | Comparatively complex. Can’t hold a lot of information. Inappropriate for sensitive data. Easily modified by the end user. |
HTML5 Web Storage | Simple API to store name-value pairs | Data is never automatically sent to the server, so mostly used for client logic only. |
On the client side, every option costs you in bandwidth. Each option involves passing data back and forth from client to server. Every byte of data you store will be paid for twice: once when it is passed to the server and once when it is passed back.
These tables provide you with some of the server-side and client-side options. Chapter 22 covers the improvements to caching in ASP.NET 4.x.
In classic ASP, the Session object was held in-process (as was everything) to the IIS process. The user received a cookie with a unique key in the form of a GUID. The session key was an index into a dictionary where object references could be stored.
In all versions of ASP.NET the Session object offers an in-process option, but also includes an out-of-process and a database-backed option. Additionally, the developer has the option to enable a cookieless session state where the session key appears in the URL rather than being sent as a cookie.
The HttpApplication object raises a series of events during the life of the HTTP protocol request; this section mentions all of them, but only provides more details on those events that are related to sessions:
By the time your application code executes, the Session object has been populated using the session key that was present in the cookie or, as you see later, from the URL. If you want to handle some processing at the time the session begins, rather than handling it in AcquireRequestState, you can define an event handler for the Start event of a session state HttpModule.
void Session_OnStart() {
'this fires after session state has been acquired by the SessionStateModule.
}
The HttpSessionState object can be used within any event in a subclass of the Page object. The pages you create in ASP.NET derive from System.Web.UI.Page, and you can access session state as a collection because System.Web.SessionState.HttpSessionState implements ICollection.
The Page has a public property aptly named Session that automatically retrieves the Session from the current HttpContext. Even though it seems as if the Session object lives inside the page, it actually lives in the HttpContext, and the page’s public Session property actually retrieves the reference to the session state. This convenience not only makes it more comfortable for the classic ASP programmer, but saves you a little typing as well.
The Session object can be referred to within a page in this way:
Session["SomeSessionState"] = "Here is some data";
or
HttpContext.Current.Session["SomeSessionState"] = "Here is some data";
The fact that the Session object actually lives in the current HTTP context is more than just a piece of trivia. This knowledge enables you to access the Session object in contexts other than the page (such as in your own HttpHandler).
All the code within a page refers to the Session object using the dictionary-style syntax seen previously, but the HttpSessionState object uses a provider pattern to extract possible choices for session state storage. You can choose between the included providers by changing the sessionState element in the web.config file. ASP.NET ships with the following three storage providers:
The format of the web.config file’s sessionState element is shown in the following code:
<configuration>
<system.web>
<sessionState mode="Off|InProc|StateServer|SQLServer|Custom" ../>
</system.web>
Begin configuring session state by setting the mode="InProc" attribute of the sessionState element in the web.config file of a new website. This is the most common configuration for session state within ASP.NET and is also the fastest, as you see next.
When the configuration is set to InProc, session data is stored in the HttpRuntime’s internal cache in an implementation of ISessionStateItemCollection that implements ICollection. The session state key is a 120-bit value string that indexes this global dictionary of object references. When session state is in process, objects are stored as live references. This mechanism is incredibly fast because no serialization occurs, nor do objects leave the process space. Certainly, your objects are not garbage-collected if they exist in the In-Process Session object because a reference is still being held.
Additionally, because the objects are stored (held) in memory, they use up memory until that session times out. If a user visits your site and hits one page, he might cause you to store a 50MB XmlDocument object in an in-process session. If that user never comes back, you are left sitting on that large chunk of memory for the next 20 minutes or so (a configurable value) until the session ends.
Although the InProc session model is the fastest, the default, and the most common, it does have a significant limitation. If the worker process or application domain recycles, all session state data is lost. In addition, the ASP.NET application may restart for a number of reasons, such as the following:
This said, in-process session state works great for smaller applications that require only a single web server, or in situations where IP load balancing is returning each user to the server where his original session was created.
Now imagine that a user already has a session key, but is returned to a different machine than the one on which his session was created. In that case the target machine does not know the session key, therefore a new session is created. The basis for this new session is the session ID supplied by the user. While the session key may be the same, the target machine did not know this session before and so does not have any data associated with the session key. The new session is empty and unexpected results may occur. There is a solution, however: If regenerateExpiredSessionId is set to True in the web.config file, a new session ID is created and assigned to the user.
Web gardening is a technique for multiprocessor systems wherein multiple instances of the ASP.NET worker process are started and assigned with processor affinity. On a larger web server with as many as four CPUs, you can have anywhere from one to four worker processes hosting ASP.NET. Processor affinity means literally that an ASP.NET worker process has an affinity for a particular CPU. It’s “pinned” to that CPU. This technique is usually enabled only in very large web farms.
Do not forget that in-process session state is just that — in-process. Even if your web application consists of only a single web server and all IP traffic is routed to that single server, you have no guarantee that each subsequent request will be served on the same processor. A web garden must follow many of the same rules that a web farm follows.
In the following simple example, in a Button_Click event, the content of the textbox is added to the Session object with a specific key. The user then clicks to go to another page within the same application, and the data from the Session object is retrieved and presented in the browser.
Note the use of the <asp:HyperLink> control. Certainly, that markup could have been hard-coded as HTML, but this small distinction will serve you well later. Additionally, the URL is relative to this site, not absolute. Watch for it to help you later in this chapter.
Listing 21-1 illustrates how simple using the Session object is. It behaves like any other IDictionary collection and allows you to store keys of type String associated with any kind of object. The retrieval file that’s referenced is added in Listing 21-2.
LISTING 21-1: Setting values in session state
VB
Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles Button1.Click
Session("mykey") = TextBox1.Text
End Sub
<html>
<head id="Head1" runat="server">
<title>Session State</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:TextBox ID="TextBox1" Runat="server"></asp:TextBox>
<asp:Button ID="Button1" Runat="server" Text="Store in Session"
OnClick="Button1_Click" />
<br />
<asp:HyperLink ID="HyperLink1" Runat="server"
NavigateUrl="Listing 21-02.aspx">Next Page</asp:HyperLink>
</div>
</form>
</body>
</html>
C#
<%@ Page Language="C#" %>
<!DOCTYPE html>
<script runat="server">
protected void Button1_Click(object sender, EventArgs e)
{
Session["mykey"] = TextBox1.Text;
}
</script>
The page from Listing 21-1 renders in the browser as shown in Figure 21-2. The Session object is accessed as any dictionary indexed by a string key. You can also use methods like Add(), Count() and others, yet the syntax above is the most common one.
More details about the page and the Session object can be displayed to the developer if page tracing is enabled. You add this element to your application’s web.config file inside the <system.web> element, as follows:
<trace enabled="true" pageOutput="true" />
Now tracing is enabled, and the tracing output is sent directly to the page. More details on tracing and debugging are given in Chapter 29. For now, make this change and refresh your browser.
In Figure 21-3, the screenshot is split by a diagonal line across the screenshot to show both the top and roughly the middle of the large amount of trace information that is returned when trace is enabled. Session state is very much baked into the fabric of ASP.NET. You can see in the Request Details section of the trace that not only was this page the result of an HTTP GET but the session ID was as well — elevated to the status of first-class citizen. However, the ASP.NET session ID lives as a cookie by default, as you can see in the Cookies collection at the bottom of the figure.
The default name for that cookie is ASP.NET_SessionId, but its name can be configured via the cookieName attribute of the <sessionState> element in the web.config file. Some large enterprises allow only certain named cookies past their proxies, so you might need to change this value when working on an extranet or a network with a gateway server; but this would be a very rare occurrence. The cookieName is changed to use the name “Foo” in the following example:
<sessionState cookieName="Foo" mode="InProc" />
The trace output shown in Figure 21-3 includes a section listing the contents of the Session State collection. In the figure, you can see that the name mykey and the value Hanselman are currently stored. Additionally, you see the CLR data type of the stored value; in this case, it’s System.String.
Now add the next page, which pulls this value out of the session. Create a new ASP.NET page, add a label, and then add a Page_Load event handler, as shown in Listing 21-2.
LISTING 21-2: Retrieving values from the session
VB
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles Me.Load
Dim myValue As String = CType(Session("mykey"), String)
Label1.Text = myValue
End Sub
<!DOCTYPE html>
<html>
<head id="Head1" runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Label ID="Label1" runat="server" />
</div>
</form>
</body>
</html>
C#
<%@ Page Language="C#" %>
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
string myValue = (string)Session["mykey"];
Label1.Text = myValue;
}
</script>
Because the session contains object references, the resulting object is converted to a string by way of a cast in C# or the CType or CStr function in VB.
It is unfortunate that you can’t store and retrieve values of a specific data type in the Session object without using any type casting syntax, since everything is getting stored as an object. Combined with the string key used as an index, it makes for a fairly weak contract between the page and the Session object. You can create a session helper that is specific to your application to hide these details, or you can add properties to a base Page class that presents these objects to your pages in a friendlier way. Because the generic Session object is available as a property on System.Web.UI.Page, add a new class derived from Page that exposes a new property named MyKey.
Start by right-clicking your project and selecting Add New Item from the context menu to create a new class. Name it SmartSessionPage and click OK. The IDE may tell you that it would like to put this new class in the App_Code folder to make it available to the whole application. Click Yes.
Your new base page is very simple. Via derivation, it does everything that System.Web.UI.Page does, plus it has a new property, as shown in Listing 21-3 (code file SmartSessionPage.cs and SmartSessionPage.vb).
LISTING 21-3: A more session-aware base page
VB
Imports Microsoft.VisualBasic
Imports System
Imports System.Web
Public Class SmartSessionPage
Inherits System.Web.UI.Page
Private Const MYSESSIONKEY As String = "mykey"
Public Property MyKey() As String
Get
Return CType(Session(MYSESSIONKEY), String)
End Get
Set(ByVal value As String)
Session(MYSESSIONKEY) = value
End Set
End Property
End Class
C#
using System;
using System.Web;
public class SmartSessionPage : System.Web.UI.Page
{
private const string MYKEY = "mykey";
public string MyKey
{
get
{
return (string)Session[MYKEY];
}
set
{
Session[MYKEY] = value;
}
}
}
Now, return to your code from Listing 21-1 and derive your pages from this new base class. To do this, change the base class to inherit from SmartSessionPage. Listing 21-4 shows how the page derives from the SmartSessionPage, which in turn derives from System.Web.UI.Page. Listing 21-4 outlines the changes to make to Listing 21-1.
LISTING 21-4: Deriving from the new base page
VB
<%@ Page Language="VB" Inherits="SmartSessionPage" %>
Partial Class _Default
Inherits SmartSessionPage
Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs)
' Session("mykey") = TextBox1.Text
MyKey = TextBox1.Text
End Sub
End Class
C#
<%@ Page Language="C#" Inherits="SmartSessionPage" %>
<script runat="server">
protected void Button1_Click(object sender, EventArgs e)
{
//Session["mykey"] = TextBox1.Text;
MyKey = TextBox1.Text;
}
</script>
In this code, you change the access to the Session object so it uses the new public property. After the changes in Listing 21-3, all derived pages have a public property called MyKey. This property can be used without any concern about casting or session key indexes. Additional specific properties can be added as other objects are included in the session.
By default, all pages have write access to the Session. Because it’s possible that more than one page from the same browser client might be requested at the same time (using frames, more than one browser window on the same machine, and so on), a page holds a reader/writer lock on the same session for the duration of the page request. If a page has a writer lock on the same session, all other pages requested in the same session must wait until the first request finishes. To be clear, the session is locked only for that session ID. These locks do not affect other users with different sessions.
In order to get the best performance out of your pages that use session, ASP.NET allows you to declare exactly what your page requires of the Session object via the EnableSessionState @Page attribute. The options are True, False, or ReadOnly:
By modifying the @Page direction to reflect each page’s actual need, you affect performance when the site is under load. Add the EnableSessionState attribute to the pages, as shown in the following code:
<%@ Page Language="VB" EnableSessionState="True" %>
<%@ Page Language="C#" EnableSessionState="ReadOnly" %>
Under the covers, ASP.NET is using marker interfaces from the System.Web.SessionState namespace to keep track of each page’s needs. When the partial class for Default.aspx is generated, it implements the IRequiresSessionState interface, whereas Retrieve.aspx implements IReadOnlySessionState. All HttpRequests are handled by objects that implement IHttpHandler. Pages are handled by a PageHandlerFactory. You can find more on HttpHandlers in Chapter 30. Internally, the SessionStateModule is executing code similar to the pseudo-code that follows:
If TypeOf HttpContext.Current.Handler Is IReadOnlySessionState Then
Return SessionStateStore.GetItem(itemKey)
ElseIf TypeOf HttpContext.Current.Handler Is IRequiresSessionState
Return SessionStateStore.GetItemExclusive(itemKey)
End If
As the programmer, you know things about the intent of your pages at compile time that ASP.NET cannot figure out at run time. By including the EnableSessionState attribute in your pages, you allow ASP.NET to operate more efficiently. Remember, ASP.NET always makes the most conservative decision unless you give it more information to act upon.
Out-of-process session state is held in a process called aspnet_state.exe that runs as a Windows Service. You can start the ASP.NET state service by using the Services MMC snap-in or by running the following net command from an administrative command line:
net start aspnet_state
By default, the state service listens on TCP port 42424, but this port can be changed at the registry key for the service, as shown in the following code. The state service is not started by default.
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\aspnet_state\Parameters\Port
Change the web.config file’s settings from InProc to StateServer, as shown in the following code. Additionally, you must include the stateConnectionString attribute with the IP address and port on which the session state service is running. In a web farm (a group of more than one web server), you could run the state service on any single server or on a separate machine entirely. In this example, the state server is running on the local machine, so the IP address is the localhost IP 127.0.0.1. If you run the state server on another machine, make sure the appropriate port is open — in this case, TCP port 42424.
<configuration>
<system.web>
<sessionState mode="StateServer"
stateConnectionString="tcpip=127.0.0.1:42424"/>
</system.web>
</configuration>
The state service used is always the most recent one installed with ASP.NET. That means that if you are running ASP.NET 2.0/3.5/4 or 4.5 and 1.1 on the same machine, all the states stored in Session objects for any and all versions of ASP.NET are kept together in a single instance of the ASP.NET state service, and the service used for handling sessions belongs to the latest version of ASP.NET.
Because your application’s code runs in the ASP.NET worker process (aspnet_wp.exe, or w3wp.exe) and the state service runs in the separate aspnet_state.exe process, objects stored in the session cannot be stored as references. Your objects must physically leave the worker process via binary serialization.
Only classes that have been marked with the [Serializable] attribute may be serialized. In the context of the Session object, think of the [Serializable] attribute as a permission slip for instances of your class to leave the worker process. This is especially important if you have used InProc sessions before, since they do not have this requirement. So you might change your code, if you change your session state mode.
Update your App_Code directory to include a new class called Person, as shown in Listing 21-5. Be sure to mark it as [Serializable] or you will see the error shown in Figure 21-4.
As long as you have marked your objects as [Serializable], they will be allowed out of the ASP.NET process. Notice that the objects in Listing 21-5 (code files Person.cs and Person.vb) are marked [Serializable].
LISTING 21-5: A serializable object that can be used in the out-of-process session
VB
Imports Microsoft.VisualBasic
Imports System
Imports System.Web
<Serializable()> _
Public Class Person
Public firstName As String
Public lastName As String
Public Overrides Function ToString() As String
Return String.Format("Person Object: {0} {1}", firstName, lastName)
End Function
End Class
C#
using System;
using System.Web;
[Serializable]
public class Person
{
public string firstName;
public string lastName;
public override string ToString()
{
return String.Format("Person Object: {0} {1}", firstName, lastName);
}
}
Because you put an instance of the Person class from Listing 21-5 into the Session object that is currently configured as StateServer, you should add a strongly typed property to the base Page class from Listing 21-3. In Listing 21-6 you see the strongly typed property added (the class is renamed SmartSessionPage2 to make it easily distinguishable). You can find the code for Listing 21-6 in code files SmartSessionPage2.cs and SmartSessionPage2.vb. Note the cast on the property Get, and the strongly typed return value indicating that this property deals only with objects of type Person.
LISTING 21-6: Adding a strongly typed property to SmartSessionPage
VB
Imports Microsoft.VisualBasic
Imports System
Imports System.Web
Public Class SmartSessionPage2
Inherits System.Web.UI.Page
Private Const MYSESSIONPERSONKEY As String = "myperson"
Public Property MyPerson() As Person
Get
Return CType(Session(MYSESSIONPERSONKEY), Person)
End Get
Set(ByVal value As Person)
Session(MYSESSIONPERSONKEY) = value
End Set
End Property
End Class
C#
using System;
using System.Web;
public class SmartSessionPage2 : System.Web.UI.Page
{
private const string MYPERSON = "myperson";
public Person MyPerson
{
get
{
return (Person)Session[MYPERSON];
}
set
{
Session[MYPERSON] = value;
}
}
}
Now, add code to create a new Person, populate its fields from the textbox, and put the instance into the now out-of-process session state service. Then, retrieve the Person and write its values out to the browser using the overloaded ToString() method from Listing 21-5.
In Listing 21-7, the value of the TextBox is split into a string array and the first two strings are put into a Person instance. For example, if you entered "Christian Wenz" as a value, "Christian" is put into Person .firstName and "Wenz" is put into Person.lastName. The values you enter should appear when they are retrieved later in Listing 21-8 and written out to the browser.
LISTING 21-7: Setting objects from the session using state service and a base page
VB
Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles Button1.Click
Dim names As String()
names = TextBox1.Text.Split(" "c) ' " "c creates a char
Dim p As New Person() With {
.firstName = names(0),
.lastName = names(1)
}
MyPerson = p
End Sub
<!DOCTYPE html>
<html>
<head id="Head1" runat="server">
<title>Session State</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:TextBox ID="TextBox1" Runat="server"></asp:TextBox>
<asp:Button ID="Button1" Runat="server" Text="Store in Session"
OnClick="Button1_Click" />
<br />
<asp:HyperLink ID="HyperLink1" Runat="server"
NavigateUrl="Listing 21-08.aspx">Next Page</asp:HyperLink>
</div>
</form>
</body>
</html>
C#
<%@ Page Language="C#" Inherits="SmartSessionPage2" %>
<script runat="server">
protected void Button1_Click(object sender, EventArgs e)
{
string[] names = TextBox1.Text.Split(' ');
Person p = new Person()
{
firstName = names[0],
lastName = names[1]
};
MyPerson = p;
}
</script>
LISTING 21-8: Retrieving objects from the session using state service and a base page
VB
<%@ Page Language="C#" Inherits="SmartSessionPage2" %>
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles Me.Load
Dim p As Person = MyPerson
Label1.Text = p.ToString()
End Sub
</script>
<!DOCTYPE html>
<html>
<head id="Head1" runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Label ID="Label1" runat="server" />
</div>
</form>
</body>
</html>
C#
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
Person p = MyPerson;
Label1.Text = p.ToString();
}
</script>
Now, launch the browser with Listing 21-7, enter your name, click the button to store it in the Session object, and then visit Listing 21-8 via the hyperlink. You see a result, as shown in Figure 21-5.
The completed code and techniques shown in Listings 21-7 and 21-8 illustrate a number of best practices for session management:
These best practices apply to all state storage methods, including SQL session state (covered shortly). When using out-of-process session state, whether as described previously or using the SQL-backed session state, the objects that are stored within this state are serialized and deserialized back and forth in order for your applications to work with this type of state. Some of the objects that you are moving to be serialized into memory can be quite large.
ASP.NET 4.x includes a capability to compress the objects that are stored in an out-of-process state. This is illustrated in the following snippet of code:
<sessionState
mode="SqlServer"
sqlConnectionString="data source=dbserver;Initial Catalog=aspnetstate"
allowCustomSqlDatabase="true"
compressionEnabled="true"
/>
When compression is enabled through the use of the compressionEnabled attribute, the System.IO.Compression.GZipStream class is used for compressing the object. By default, the compressionEnabled attribute is set to False. By doing this compression, you will notice a significant difference in the amount of memory used to store your state.
ASP.NET sessions can also be stored in a SQL Server database. InProc offers speed, state server offers a resilience/speed balance, and storing sessions in SQL Server offers resilience that can serve sessions to a large web farm that persists across IIS restarts, if necessary.
SQL-backed session state is configured with aspnet_regsql.exe. This tool adds and removes support for a number of ASP.NET features such as cache dependency (see Chapter 22) and personalization/membership (see Chapters 18 and 19), as well as session support. When you run aspnet_regsql.exe from the command line without any options, surprisingly, it pops up a GUI, as shown in Figure 21-6. This utility is located in the .NET Framework’s installed directory, usually something like C:\Windows\Microsoft.NET\Framework\v4.0.30319.
The text of the dialog box shown in Figure 21-6 contains instructions to run aspnet_regsql from the command line with a -? switch. You have a huge number of options, so you will want to pipe it through in a form like aspnet_regsql -? | more. Here are the session state–specific options:
-- SESSION STATE OPTIONS --
-ssadd Add support for SQLServer mode session state.
-ssremove Remove support for SQLServer mode session state.
-sstype t|p|c Type of session state support:
t: temporary. Session state data is stored in the
"tempdb" database. Stored procedures for managing
session are installed in the "ASPState" database.
Data is not persisted if you restart SQL. (Default)
p: persisted. Both session state data and the stored
procedures are stored in the "ASPState" database.
c: custom. Both session state data and the stored
procedures are stored in a custom database. The
database name must be specified.
-d <database> The name of the custom database to use if -sstype is "c"
Three options exist for session state support: t, p, and c. The most significant difference is that the -sstype t option does not persist session state data across SQL Server restarts, whereas the -sstype p option does. Alternatively, you can specify a custom database with the -c option and give the database name with -d database.
The following command-line example configures your system for SQL session support with the SQL Server on localhost with an sa password of wrox and a persistent store in the ASPState database. (Certainly, you know not to deploy your system using sa and a weak password, but this simplifies the example. Ideally, you would use Windows Integration Authentication and give the Worker Process identity access to the ASPState database.) If you are using SQL Express, replace localhost with .\SQLEXPRESS. If you are not using Windows Authentication, you may need to explicitly enable the sa account from the Management Studio, run this tool, and then disable the sa account for security reasons.
C:\>aspnet_regsql -S localhost -U sa -P wrox -ssadd -sstype p
Microsoft (R) ASP.NET SQL Registration Tool version 4.0.30319.17929
Administrative utility to install and uninstall ASP.NET features on a SQL server.
Copyright (C) Microsoft Corporation. All rights reserved.
Start adding session state.
. . . . . . . . . . .
Finished.
When using a trusted connection (and Windows authentication), use the -E switch:
C:\>aspnet_regsql -S localhost -E -ssadd -sstype p
Next, open SQL Management Studio (or its Express version) and look at the newly created database. Two tables are created — ASPStateTempApplications and ASPStateTempSessions — as well as a series of stored procedures to support moving the session back and forth from SQL to memory.
If your SQL Server has its security locked down tight, you might get an error 15501 after executing aspnet_regsql.exe that says, “An error occurred during the execution of the SQL file ‘InstallSqlState.sql’.” The SQL error number is 15501 and the SqlException message is
This module has been marked OFF. Turn on 'Agent XPs' in order to be able to access
the module. If the job does not exist, an error from msdb.dbo.sp_delete_job
is expected.
This message is rather obscure, but aspnet_regsql.exe is trying to tell you that the extended stored procedures it needs to enable session state are not enabled for security reasons. You have to allow them explicitly. To do so, execute the following commands within the SQL Server Management Studio Express:
USE master
EXECUTE sp_configure 'show advanced options', 1
RECONFIGURE WITH OVERRIDE
GO
EXECUTE sp_configure 'Agent XPs', 1
RECONFIGURE WITH OVERRIDE
GO
EXECUTE sp_configure 'show advanced options', 0
RECONFIGURE WITH OVERRIDE
GO
Now, change the web.config <sessionState> element to use SQL Server, as well as the new connection string:
<sessionState mode="SQLServer" sqlConnectionString="data source=127.0.0.1;user
id=sa;password=Wrox"/ >
For a trusted connection, the connection string could look like this:
<sessionState mode="SQLServer"
sqlConnectionString="data source=127.0.0.1;trusted_connection=yes"/ >
The session code shown in Listings 21-7 and 21-8 continues to work as before. However, if you open the ASPStateTempSessions table, you see the serialized objects. Notice in Figure 21-7 that the session ID from the trace appears as a primary key in a row in the ASPStateTempSessions table.
Figure 21-7 shows the SessionId as seen in the Request Details of ASP.NET tracing. That SessionId appears in the SessionId column of the ASPStateTempSessions table in the ASPState database just created. Notice also the ASPStateTempApplications table keeps track of each IIS application that may be using the same database to manage sessions.
If you want to use your own database to store session state, you specify the database name with the -d <database> switch of aspnet_regsql.exe and include the allowCustomSqlDatabase="true" attribute and the name of the database in the connection string:
<sessionState allowCustomSqlDatabase="true" mode="SQLServer"
sqlConnectionString="data source=127.0.0.1; database=MyCustomASPStateDatabase;" />
The user ID and password can be included in the connection string; or Windows Integrated Security can be used if the ASP.NET Worker Process’s identity is configured with access in SQL Server.
ASP.NET session state is built on an extensible, provider-based storage model. You can implement custom providers that store session data in other storage mechanisms simply by deriving from SessionStateStoreProviderBase. This extensibility feature also allows you to generate session IDs via your own algorithms by implementing ISessionIDManager.
You start by creating a class that inherits from SessionStateStoreProviderBase. The session module will call methods on any session provider as long as it derives from SessionStateStoreProviderBase. Register your custom provider in your application’s web.config file, as in the following example:
<sessionState mode ="Custom" customProvider ="WroxProvider">
<providers >
<add name="WroxProvider" type="Wrox.WroxStore, WroxSessionSupplier"/>
</providers>
< /sessionState >
ASP.NET initializes the SessionStateModule, and these methods are called on any custom implementation:
When a session item is requested, ASP.NET calls your implementation to retrieve it. Implement the following methods to retrieve items:
When it’s time to store an item, the following method is called:
In the previous example, the ASP.NET session state ID was stored in a cookie. Some devices do not support cookies, or a user may have turned off cookie support in her browser. Cookies are convenient because the values are passed back and forth with every request and response. That means every HttpRequest contains cookie values, and every HttpResponse contains cookie values. What is the only other thing that is passed back and forth with every Request and Response? The answer is the URL.
If you include the cookieless="UseUri" attribute in the web.config file, ASP.NET does not send the ASP.NET session ID back as a cookie. Instead, it modifies every URL to include the session ID just before the requested page:
<sessionState mode="SQLServer" cookieless="UseUri" sqlConnectionString="data
source=127.0.0.1;user id=sa;password=Wrox" />
Notice that the session ID appears in the URL as if it were a directory of its own situated between the actual website virtual directory and the page. With this change, server-side user controls such as the HyperLink control, used in Listing 21-1, have their properties automatically modified. The link in Listing 21-1 could have been hard-coded as HTML directly in the Designer, but then ASP.NET could not modify the target URL shown in Figure 21-8.
The session ID is a string that contains only the ASCII characters allowed in a URL. That makes sense when you realize that moving from a cookie-based session state system to a cookieless system requires putting that session state value in the URL.
Notice in Figure 21-8 that the request URL contains a session ID within parentheses. One disadvantage to cookieless sessions is how easily they can be tampered with. Certainly, cookies can be tampered with using HTTP sniffers, but URLs can be edited by anyone. The only way session state is maintained is if every URL includes the session ID in this way.
Additionally, all URLs must be relative. Remember that the session ID appears as if it were a directory. The session is lost if an absolute URL, such as /myapp/my/file.aspx, is invoked. If you are generating URLs on the server side, use HttpResponse.ApplyAppPathModifier(). It changes a URL when the session ID is embedded, as shown here:
Response.Write(Response.ApplyAppPathModifier("my/file.aspx"));
The previous line generates a URL similar to the following:
/myapp/(S(avkbnbml4n1n5mi5dmfqnu45))/my/file.aspx
Notice that not only was session information added to the URL, but it was also converted from a relative URL to an absolute URL, including the application’s virtual directory. This method can be useful when you need to use Response.Redirect or build a URL manually to redirect from an HTTP page to an HTTPS page while still maintaining a cookieless session state.
Now that you are familiar with the variety of options available for maintaining state in ASP.NET, here’s some real-world advice from production systems. The in-process (InProc) session provider is the fastest method, of course, because everything held in memory is a live object reference. This provider is held in the HttpApplication’s cache and, as such, it is susceptible to application recycles. If you use Windows 2000 Server or Windows XP, the aspnet_wp.exe process manages the ASP.NET HTTP pipeline. If you are running a more recent version like Windows Server 2008, or 2012, or Windows 7 or 8, w3wp.exe is the default process that hosts the run time.
You must find a balance between the robustness of the out-of-process state service and the speed of the in-process provider. In the authors’ experience, the out-of-process state service is usually about 15 percent slower than the in-process provider because of the serialization overhead and the marshaling that needs to occur. SQL session state is about 25 percent slower than InProc. Of course, your mileage will likely vary. Do not let these numbers concern you too much. Be sure to do scalability testing on your applications before you panic and make inappropriate decisions.
The Application object is the equivalent of a bag of global variables for your ASP.NET application. Global variables have been considered harmful for many years in other programming environments, and ASP.NET is no different. You should give some thought to what you want to put in the Application object and why. Often, the more flexible Cache object that helps you control an object’s lifetime is the more useful. Caching is discussed in depth in Chapter 22.
The Application object is not global to the machine; it is global to the HttpApplication. If you are running in the context of a web farm, each ASP.NET application on each web server has its own Application object. Because ASP.NET applications are multithreaded and are receiving requests that are being handled by your code on multiple threads, access to the Application object should be managed using the Application.Lock and Application.Unlock methods. If your code does not call Unlock directly (which it should, shame on you) the lock is removed implicitly at the end of the HttpRequest that called Lock originally.
This small example shows you how to lock the Application object just before inserting an object. Other threads that might be attempting to write to the Application will wait until it is unlocked. This example assumes there is an integer already stored in Application under the key GlobalCount.
VB
Application.Lock()
Application("GlobalCount") = CType(Application("GlobalCount"), Integer) + 1
Application.UnLock()
C#
Application.Lock();
Application["GlobalCount"] = (int)Application["GlobalCount"] + 1;
Application.UnLock();
Object references can be stored in the Application, as in the Session, but they must be cast back to their known types when retrieved (as shown in the preceding sample code).
The URL, or QueryString, is the ideal place for navigation-specific — not user-specific — data. The QueryString is the most hackable element on a website, and that fact can work for you or against you. For example, if your navigation scheme uses your own page IDs at the end of a query string (such as /mysite/mypage.aspx?id=54) be prepared for a user to play with that URL in his browser, and try every value for id under the sun. Do not blindly cast id to an int, and if you do, have a plan if it fails. A good idea is to return Response.StatusCode=404 when someone changes a URL to an unreasonable value. Another fine idea that Amazon.com implemented was the Smart 404. Perhaps you have seen these: They say “Sorry you didn’t find what you’re looking for. Did you mean _______?”
Remember, your URLs are the first thing your users may see, even before they see your HTML. Hackable URLs — hackable even by my mom — make your site more accessible. Which of these URLs is friendlier and more hackable (for the right reason)?
http://reviews.cnet.com/Philips_42PF9996/4505-6482_7-31081946.html?tag=cnetfd.sd
or
http://www.hanselman.com/blog/CategoryView.aspx?category=Movies
Remember when cookies were first introduced. Most users were not quite sure just what a cookie was, but they were all convinced that cookies were evil and were storing their personal information. Back then, it was likely that personal information was stored in the cookie! Never, ever store sensitive information, such as a user ID or password, in a cookie. Cookies should be used to store only non-sensitive information, or information that can be retrieved from an authoritative source. Cookies should not be trusted, and their contents should be able to be validated. For example, if a Forms Authentication cookie has been tampered with, the user is logged out and an exception is thrown. If an invalid session ID cookie is passed in for an expired session, a new cookie can be assigned.
When you store information in cookies (using Response.Cookies–reading is done using Request.Cookies), remember that it is quite different from storing data in the Session object:
ASP.NET uses the concept of the postback, wherein a server-side event is raised to alert the developer of a client-side action. If a button is clicked on the browser, the Form collection is POSTed back to the server, but now ASP.NET allows the developer to write code in events such as Button1_Click() and TextBox1_Changed().
However, this technique of posting back to the same page is counterintuitive, especially when you are designing user interfaces that aim to create wizards to give the user the sense of forward motion.
This chapter is about all aspects of state management. Postbacks and cross-page postbacks, however, are covered extensively in Chapter 3, so this chapter touches on them only in the context of state management. Postbacks were introduced in ASP.NET 1.x to provide an eventing subsystem for web development. Having only single-page postbacks in 1.x was inconvenient, however, and that caused many developers to store small objects in the session on a postback and then redirect to the next page to pick up the stored data. With cross-page postbacks, data can be posted “forward” to a different page, often obviating the need for storing small bits of data that could otherwise be passed directly.
ASP.NET 2.0 and above includes the notion of a PostBackUrl to all the Button controls including LinkButton and ImageButton. The PostBackUrl property is both part of the markup when a control is presented as part of the ASPX page, as shown in the following code snippet, and is a property on the server-side component that is available in the code-behind:
<asp:Button PostBackUrl="url" . . .>
When a button control with the PostBackUrl property set is clicked, the page does not post back to itself; instead, the page is posted to the URL assigned to the button control’s PostBackUrl property. When a cross-page request occurs, the PreviousPage property of the current Page class holds a reference to the page that caused the postback. To get a control reference from the PreviousPage, use the Controls property, or use the FindControl() method (which requires casting), or the @PreviousPageType attribute.
Create a new page (as shown in Listing 21-9). Put a TextBox and a Button on it, and set the Button PostBackUrl property to Listing 21-10.aspx. Then create a Listing 21-10.aspx page with a single Label and add a Page_Load handler by double-clicking the HTML Designer. Listing 21-10 then contains the code to receive and process the postback.
LISTING 21-9: Cross-page postbacks: The sender
<%@ Page Language="C#" %>
<!DOCTYPE html>
<script runat="server">
</script>
<html>
<head id="Head1" runat="server">
<title>Cross-page PostBacks</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:TextBox ID="TextBox1" Runat="server"></asp:TextBox>
<asp:Button ID="Button1" Runat="server" Text="Button"
PostBackUrl="~/Listing 21-10.aspx" />
</div>
</form>
</body>
</html>
LISTING 21-10: Cross-page postbacks: The receiver
VB
<%@ Page Language="C#" %>
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles Me.Load
If PreviousPage IsNot Nothing AndAlso PreviousPage.IsCrossPagePostBack Then
Dim text As TextBox = _
CType(PreviousPage.FindControl("TextBox1"), TextBox)
If text IsNot Nothing Then
Label1.Text = text.Text
End If
End If
End Sub
</script>
<html>
<head id="Head1" runat="server">
<title>Step 2</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label></div>
</form>
</body>
</html>
C#
<%@ Page Language="C#" %>
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
if (PreviousPage != null && PreviousPage.IsCrossPagePostBack)
{
TextBox text = PreviousPage.FindControl("TextBox1") as TextBox;
if (text != null)
{
Label1.Text = text.Text;
}
}
}
</script>
In Listing 21-9, the page posts forward to Listing 21-10, which can then access the Page.PreviousPage property and retrieve a populated instance of the Page, which caused the postback. A call to FindControl and a cast retrieves the TextBox from the previous page and copies its value into the label of Listing 21-10.
Hidden input fields such as <input type="hidden" name="myName" /> are sent back as name/value pairs in a form POST exactly like any other control, except that they are not rendered. Think of them as hidden textboxes. Figure 21-9 shows a HiddenField control on the Visual Studio Designer with its available properties. Hidden fields are available in all versions of ASP.NET.
ViewState, on the other hand, exposes itself as a collection of key/value pairs like the Session object, but renders itself as a hidden field with the name "__VIEWSTATE" like this:
<input type="hidden" name="__VIEWSTATE" value="/AAASSDAS . . . Y/lOI=" />
Any objects put into the ViewState must be marked Serializable. ViewState serializes the objects with a special binary formatter called the LosFormatter. LOS stands for limited object serialization. It serializes any kind of object, but it is optimized to contain strings, arrays, and hashtables.
To see this at work, create a new page and drag a TextBox, Button, and HiddenField onto it. Double-click in the Designer to create a Page_Load and include the code from Listing 21-11. This example adds a string to HiddenField.Value, but adds an instance of a Person to the ViewState collection. This listing illustrates that while ViewState is persisted in a single HTML TextBox on the client, it can contain both simple types such as strings, and complex types such as Person. This technique has been around since ASP .NET 1.x and continues to be a powerful and simple way to persist small pieces of data without utilizing server resources.
LISTING 21-11: Hidden fields and ViewState
VB
<%@ Page Language="C#" %>
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles Me.Load
If Not Page.IsPostBack Then
HiddenField1.Value = "value 1"
ViewState("AnotherHiddenValue") = "value 2"
Dim p As New Person() With {
.firstName = "Christian",
.lastName = "Wenz"
}
ViewState("HiddenPerson") = p
End If
End Sub
</script>
<!DOCTYPE html>
<html>
<head id="Head1" runat="server">
<title>Hidden Fields and ViewState</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
<asp:Button ID="Button1" runat="server" Text="Button" />
<asp:HiddenField ID="HiddenField1" runat="server" />
</div>
</form>
</body>
</html>
C#
<%@ Page Language="C#" %>
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
HiddenField1.Value = "value 1";
ViewState["AnotherHiddenValue"] = "value 2";
Person p = new Person()
{
firstName = "Christian",
lastName = "Wenz"
};
ViewState["HiddenPerson"] = p;
}
}
</script>
In Listing 21-11, a string is added to a HiddenField and to the ViewState collection. Then, a Person instance is added to the ViewState collection with another key. A fragment of the rendered HTML is shown in the following code:
<form method="post" action="Listing 21-11.aspx" id="form1">
<div>
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="U77CNOTJ/2LWlBwWh4u
hvq+FGDtavwu+NmR3cqXa9patotBdiioisxrbCrqe3FQqBk/rvwgCnNC2VzdUcXN+bUx3+M0wZ1oqAMCTRvZ
QqnYyc/KG+YoMTo+4httTnyCfNKYBzDwZtQQhLCOJUzRPlduaS/YYFgze3t+ORW6fnRZ/ai8Pfn4LPS+ejoV
woY71mkCO2iIHLzWjU0g7lsemPJpjVl1KN+asG8IF2rb5vvGP6RPckWZ1P6e0EMWlWKZcZaaJ9j0toeEcCYZ
SU/GwGvab1SPUSV39onQ3a+UzEG0dzmg6jqaW/tQUahBkudZpZ7dbirUqaul642Q3FIjkadibrS9zUQn/KAP
898X+lQvvYgL0IOwpf1G5Jk6Xe6++" />
</div>
<div>
<input name="TextBox1" type="text" id="TextBox1" />
<input type="submit" name="Button1" value="Button" id="Button1" />
<input type="hidden" name="HiddenField1" id="HiddenField1" value="foo" />
</div>
</form>
Notice that the ViewState value uses only valid ASCII characters to represent all its contents. Do not let the sheer mass of it fool you. It is big and it appears to be opaque. However, it is just a hidden textbox and is automatically POSTed back to the server. The entire ViewState collection is available to you in the Page_Load. The value of the HiddenField is stored as plaintext.
Neither ViewState nor hidden fields are acceptable for any kind of sensitive data.
By default, the ViewState field is sent to the client with a salted hash to prevent tampering. Salting means that the ViewState’s data has a unique value appended to it before it is encoded. As Keith Brown says, “Salt is just one ingredient to a good stew.” The technique used is called HMAC, or hashed message authentication code. As shown in the following code, you can use the <machineKey> element of the web.config file to specify the validationKey, as well as the algorithm used to protect ViewState. This section of the file and the decryptionKey attribute also affect how forms authentication cookies are encrypted (see Chapter 20 for more on forms authentication).
<machineKey validationKey="AutoGenerate,IsolateApps"
decryptionKey="AutoGenerate,IsolateApps" validation="SHA1" / >
If you are running your application in a web farm, <validationKey> and <decryptionKey> have to be manually set to the same value. Otherwise, ViewState generated from one machine could be POSTed back to a machine in the farm with a different key! The keys should be 128 characters long (the maximum) and generated by random means. If you add IsolateApps to these values, ASP.NET generates a unique encrypted key for each application using each application’s application ID.
The validation attribute can be set to SHA1 or MD5 to provide tamper proofing, but you can include added protection by encrypting ViewState as well. ASP.NET offers a decryption attribute that is used exclusively for specifying the encryption and decryption mechanisms for forms authentication tickets; the validation attribute is used exclusively for ViewState, which can now be encrypted using 3DES or AES with the key stored in the validationKey attribute.
With ASP.NET 4.x, you can also add the ViewStateEncryptionMode attribute to the <pages> configuration element with two possible values, Auto or Always. Setting the attribute to Always forces encryption of ViewState, whereas setting it to Auto encrypts ViewState only if a control requested encryption uses the new Page.RegisterRequiresViewStateEncryption method.
Added protection can be applied to ViewState by setting Page.ViewStateUserKey in the Page_Init to a unique value, such as the user’s ID. This must be set in Page_Init because the key should be provided to ASP.NET before ViewState is loaded or generated. For example:
protected void Page_Init (Object sender, EventArgs e)
{
if (User.Identity.IsAuthenticated) {
ViewStateUserKey = User.Identity.Name;
}
}
When optimizing their pages, ASP.NET programmers often disable ViewState for many controls when that extra bit of state is not absolutely necessary. So that controls that require state information will still work, ASP.NET includes a second, parallel ViewState-like collection called ControlState. This dictionary can be used for round-tripping crucial information of limited size that should not be disabled even when ViewState is. You should only store data in the ControlState collection that is absolutely critical to the functioning of the control.
Recognize that ViewState, and also ControlState, although not secure, are a good place to store small bits of a data and state that don’t quite belong in a cookie or in the Session object. If the data that must be stored is relatively small and local to that specific instance of your page, ViewState is a much better solution than littering the Session object with lots of transient data.
The Items collection of HttpContext is one of ASP.NET’s best-kept secrets. It is an IDictionary key/value collection of objects that’s shared across the life of a single HttpRequest. That’s a single HttpRequest. Why would you want to store state for such a short period of time? Consider these reasons:
The Items collection holds objects, just like many of the collections that have been used in this chapter. You need to cast those objects back to their specific type when they are retrieved.
Within a web-aware database access layer, per-request caching can be quickly implemented with the simple coding pattern shown here. Note that this sample code is a design pattern, and there is no MyData class; it is for illustration.
VB
Public Shared Function GetExpensiveData(ID As Integer) As MyData
Dim key as string = "data" & ID.ToString()
Dim d as MyData = _
CType(HttpContext.Current.Items(key), MyData)
If d Is Nothing Then
d = New Data()
'Go to the Database, do whatever. . .
HttpContext.Current.Items(key) = d
End If
Return d
End Function
C#
public static MyData GetExpensiveData(int ID)
{
string key = "data" + ID.ToString();
MyData d = (MyData) HttpContext.Current.Items[key];
if (d == null)
{
d = new Data();
//Go to the Database, do whatever. . .
HttpContext.Current.Items[key] = d;
}
return d;
This code checks the Items collection of the current HttpContext to see whether the data is already there. If it’s not, the data is retrieved from the appropriate backing store and then stored in the Items collection. Subsequent calls to this function within the same HttpRequest receive the already-cached object.
As with all optimizations and caching, premature optimization is the root of all evil. Measure your need for caching and measure your improvements. Do not cache just because it feels right; cache because it makes sense.
This chapter explored the many ways to manage state within your ASP.NET application. The Session object and its providers offer many choices. Each has its own pros and cons for managing state in the form of object references and serialized objects in a way that can be made largely transparent to the application. Server-side session state data can have its unique identifying key stored in a cookie, or the key can be carried along in the URL. Cookies can also be used independently to store small amounts of data and persist it between visits, albeit in much smaller amounts and with simpler types. Hidden fields, ViewState, ControlState, postbacks, and cross-page postbacks offer possibilities for managing small bits of state within a multi-page user experience. HttpContext.Current.Items offers a perfect place to hold transient state, living the life of only a single HttpRequest. QueryStrings are an old standby for holding non-private state that is appropriate for navigation.
Subsequent versions of ASP.NET have improved on ASP.NET 1.x’s state management options with a flexible Session State Provider module, the addition of ControlState for user controls, and cross-page postbacks for a more mature programming model.