【读书笔记】ASP.NET State Management
HTTP是无状态的协议,在Web Server 响应页面请求之后,客户端和Web Server会断开,Web Server为该请求创建的页面对象会被销毁掉。这样可以保证Server可以最大化同时响应客户请求能力,同时也减少了对Server端内存开销的压力。
但是这样缺点也是显而易见的,需要用另外方法来记住页面当前的相关状态,以便Server端作相应的处理。
ASP.NET提供了以下几种方式来进行状态管理:
(1) ViewState
(2) QueryString
(3) Customer Cookies
(4) Session State
(5) Application State
(6) Profiles
(7) Caching
ViewState
ViewState 作为单个页面状态管理的首选,可以存储的内容包括简单的数据类型和自定义的对象(必须可序列化)。 ASP.NET的Page内建了ViewState这个属性。
You can store your own objects in view state just as easily as you store numeric and string types.
However, to store an item in view state, ASP.NET must be able to convert it into a stream of bytes so
that it can be added to the hidden input field in the page. This process is called serialization.
给自定义的类加上SerializableAttribute 使得对象可以序列化,
1: [Serializable]
2: public class Customer
3: {
4: public string FirstName;
5: public string LastName;
6: public Customer(string firstName, string lastName)
7: {
8: FirstName = firstName;
9: LastName = lastName;
10: }
11: }
ViewState的优缺点
ViewState 不占用Server端的内存,而且没有使用上的限制,比如说time-out限制,但是ViewState也有不适合的时候:
(1) 对于关键性的数据,最好不要用ViewState来存储,因为user可以在Postback时改变ViewState中存储的信息。这种情况下,可以考虑用Session State
(2) ViewState不适合多个页面都需要访问的数据。这种情况下,可以考虑用Session, Cookie, QueryString等
(3) 对大数据不适合。可以采用数据库存储等, 或者Session State
如何turn off ViewState
通过设置每个Control的属性EnableViewState为“FALSE”来关掉ViewState. 如果针对整个页面turn off ViewState, 可以设置Page的属性EnableViewState,或者通过Page Directive,如下
1: <%@ Page Language="C#" EnableViewState="false" ... %>
注意:即使对整个页面disable ViewState, 仍然可以看到隐藏的view state tag 保留一些少量的信息。这是因为ASP.NET总是保存关于control之间层次关系的信息。
ViewState的安全性
View state information is stored in a single Base64-encoded string that looks like this:
1: <input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="dDw3NDg2NTI5MDg7Oz4="/>
Hash codes are enabled by default, so if you want this functionality, you don’t need to take any extra steps.
To disable hash codes, you can use the EnableViewStateMAC property of the Page directive in your .aspx file:
1: <%@ Page EnableViewStateMAC="false" ... %>
Alternatively, you can set the enableViewStateMac attribute of the <pages> element in the
web.config or machine.config file, as shown here:
1: <configuration>
2: <system.web>
3: <pages enableViewStateMac="false" />
4: ...
5: </system.web>
6: </configuration>
Even when you use hash codes, the view state data will still be readable. To prevent users from getting any view state information, you can enable view state encryption. You can turn on encryption for an individual page using the ViewStateEncryptionMode property of the Page directive:
1: <%@Page ViewStateEncryptionMode="Always" ... %>
Or you can set the same attribute in the web.config configuration file:
1: <pages viewStateEncryptionMode="Always" />
在页面间传递信息
1. The Query String
The advantage of the query string is that it’s lightweight and doesn’t exert any kind of burden on the server. Unlike cross-page posting, the query string can easily transport the same information from page to page.
It has some limitations, however:
• Information is limited to simple strings, which must contain URL-legal characters.
• Information is clearly visible to the user and to anyone else who cares to eavesdrop on the Internet.
• The enterprising user might decide to modify the query string and supply new values, which your program won’t expect and can’t protect against.
• Many browsers impose a limit on the length of a URL (usually from 1 to 2 KB). For that reason, you can’t place a large amount of information in the query string and still be assured of compatibility with most browsers.
URL Encoding
The list of characters that are allowed in a URL is much shorter than the list of allowed characters in an HTML
document. All characters must be alphanumeric or one of a small set of special characters (including $-_.+!*’(),).
Furthermore, some characters have special meaning.
For example,
the ampersand (&) is used to separate multiple query string parameters,
the plus sign (+) is an alternate way to represent a space,
and the number sign (#) is used to point to a specific bookmark in a web page.
If you try to send query string values that include any of these characters, you’ll lose some of your data.
With URL encoding, special characters are replaced by escaped character sequences starting with the percent sign (%), followed by a two-digit hexadecimal representation.
The only exception is the space character, which can be represented as the character sequence %20 or the + sign.
You can use the methods of the HttpServerUtility class to encode your data automatically. For example, the following shows how you would encode a string of arbitrary data for use in the query string. This replaces all the nonlegal characters with escaped character sequences.
1: string productName = "Flying Carpet";
2: Response.Redirect("newpage.aspx?productName=" + Server.UrlEncode(productName));
You can use the HttpServerUtility.UrlDecode() method to return a URL-encoded string to its initial value. However, you don’t need to take this step with the query string because ASP.NET automatically decodes your values when you access them through the Request.QueryString collection.
Usually, it's safe to call UrlDecode() a second time, because decoding data that's already decoded won’t cause a problem. The only exception is if you have a value that legitimately includes the + sign.
In this case, calling UrlDecode() will convert the + sign to a space.
2. Cross-Page Posting
The infrastructure that supports cross-page postbacks is a property named PostBackUrl, which is defined by the IButtonControl interface and turns up in button controls such as ImageButton, LinkButton, and Button. To use cross-page posting, you simply set PostBackUrl to the name of another web form. When the user clicks the button, the page will be posted to that new URL with the values from all the input controls on the current page.
Here’s an example that defines a form with two text boxes and a button that posts to a page
named CrossPage2.aspx:
1: <%@ Page Language="C#" AutoEventWireup="true" CodeFile="CrossPage1.aspx.cs"
2: Inherits="CrossPage1" %>
3: <html xmlns="http://www.w3.org/1999/xhtml">
4: <head runat="server">
5: <title>CrossPage1</title>
6: </head>
7: <body>
8: <form id="form1" runat="server" >
9: <div>
10: <asp:TextBox runat="server" ID="txtFirstName"></asp:TextBox>
11: <asp:TextBox runat="server" ID="txtLastName"></asp:TextBox>
12: <asp:Button runat="server" ID="cmdSubmit" PostBackUrl="CrossPage2.aspx" Text="Submit" />
13: </div>
14: </form>
15: </body>
16: </html>
In CrossPage2.aspx, the page can interact with the CrossPage1.aspx objects using the
Page.PreviousPage property. Here’s an example:
1: protected void Page_Load(object sender, EventArgs e)
2: {
3: if (PreviousPage != null)
4: {
5: lblInfo.Text = "You came from a page titled " + PreviousPage.Header.Title;
6: }
7: }
ASP.NET uses some interesting sleight of hand to make this system work. The first time the second page accesses Page.PreviousPage, ASP.NET needs to create the previous page object.
To do this, it actually starts the page processing life cycle, but interrupts it just before the PreRender stage. Along the way, a stand-in HttpResponse object is created to silently catch and ignore any Response.Write() commands from the previous page.
However, there are still some interesting side effects. For example, all the page events of the previous page are fired, including Page.Load, Page.Init, and even the Button.Click event for the button that triggered the postback (if it’s defined).
Firing these events is mandatory, because they are required to properly initialize the page.
Getting Page-Specific Information
In the previous example, the information you can retrieve from the previous page is limited to the members of the Page class.
If you want to get more specific details, such as control values, you need to cast the PreviousPage reference to the appropriate type.
Here’s an example that handles this situation properly, by checking first if the PreviousPage object is an instance of the expected source (CrossPage1):
1: protected void Page_Load(object sender, EventArgs e)
2: {
3: CrossPage1 prevPage = PreviousPage as CrossPage1;
4: if (prevPage != null)
5: {
6: // (Read some information from the previous page.)
7: }
8: }
Once you’ve cast the previous page to the appropriate page type, you still won’t be able to directly access the control values. That’s because the controls are declared as protected members.
You can handle this by adding properties to the page class that wrap the control variables, like this:
1:
2: public TextBox FirstNameTextBox
3: {
4: get
5: {
6: return txtFirstName;
7: }
8: }
9:
10: public TextBox LastNameTextBox
11: {
12: get
13: {
14: return txtLastName;
15: }
16: }
A better choice is to define specific, limited methods or properties that extract just the information
you need. Here’s an example:
1: public string FullName
2: {
3: get
4: {
5: return txtFirstName.Text + " " + txtLastName.Text;
6: }
7: }
Performing Cross-Page Posting in Any Event Handler
As you learned in the previous section, cross-page posting is available only with controls that
implement the IButtonControl interface.
However, there is a workaround. You can use an overloaded method of Server.Transfer() to switch to a new ASP.NET page with the view state information left intact. You simply need to include the Boolean preserveForm parameter and set it to true, as shown here:
1: Server.Transfer("CrossPage2.aspx", true);
This gives you the opportunity to use cross-page posting anywhere in your web-page code. As with any call to Server.Transfer(), this technique causes a server-side redirect. That means there is no extra roundtrip to redirect the client. As a disadvantage, the original page URL (from the source page) remains in the user’s browser even though you’ve moved on to another page.
Interestingly, there is a way to distinguish between a cross-page post that’s initiated directly through a button and the Server.Transfer() method. Although in both cases you can access Page.PreviousPage, if you use Server.Transfer(), the Page.PreviousPage.IsCrossPagePostBack property is false. Here’s the code that demonstrates how this logic works:
1: if (PreviousPage == null)
2: {
3: // The page was requested (or posted back) directly.
4: }
5: else if (PreviousPage.IsCrossPagePostBack)
6: {
7: // A cross-page postback was triggered through a button.
8: }
9: else
10: {
11: // A stateful transfer was triggered through Server.Transfer().
12: }
The IsPostBack and IsCrossPagePostBack Properties
It’s important to understand how the Page.IsPostBack property works during a cross-page postback.
For the source page (the one that triggered the cross-page postback), the IsPostBack property is true. For the destination page (the one that’s receiving the postback), the IsPostBack property is false.
One benefit of this system is that it means your initialization code will usually run when it should.
For example, imagine CrossPage1.aspx performs some time-consuming initialization the first time it’s requested, using code like this:
1: protected void Page_Load(object sender, EventArgs e)
2: {
3: if (!IsPostBack)
4: {
5: // (Retrieve some data from a database and display it on the page.)
6: }
7: }
Now imagine the user moves from CrossPage1.aspx to CrossPage2.aspx through a cross-page postback. As soon as CrossPage2.aspx accesses the PreviousPage property, the page life cycle executes for CrossPage1.aspx. At this point, the Page.Load event fires for CrossPage1.aspx. However, on CrossPage1.aspx the Page.IsPostBack property is true, so your code skips the time-consuming initialization steps. Instead, the control values are restored from view state. On the other hand, the Page.IsPostBack property for CrossPage2.aspx is false, so this page performs the necessary first-time initialization.
In some situations, you might have code that you want to execute for the first request and all subsequent postbacks except when the page is the source of a cross-page postback. In this case, you can check the IsCrossPagePostBack property. This property is true if the current page triggered a cross-page postback.
That means you can use code like this in CrossPage1.aspx:
1: protected void Page_Load(object sender, EventArgs e)
2: {
3: if (IsCrossPagePostBack)
4: {
5: // This page triggered a postback to CrossPage2.aspx.
6: // Don't perform time-consuming initialization unless it affects
7: // the properties that the target page will read.
8: }
9: else if (IsPostBack)
10: {
11: // This page was posted back normally.
12: // Don't do the first-request initialization.
13: }
14: else
15: {
16: // This is the first request for the page.
17: // Perform all the required initialization.
18: }
19: }
Cookies
Custom cookies provide another way you can store information for later use. Cookies are small files
that are created on the client’s hard drive (or, if they’re temporary, in the web browser’s memory).
Before you can use cookies, you should import the System.Net namespace so you can easily
work with the appropriate types, as shown here:
1: using System.Net;
Cookies are fairly easy to use. Both the Request and Response objects (which are provided through Page properties) provide a Cookies collection. The important trick to remember is that you retrieve cookies from the Request object, and you set cookies using the Response object.
To set a cookie, just create a new System.Net.HttpCookie object. You can then fill it with string information (using the familiar dictionary pattern) and attach it to the current web response, as follows:
1: // Create the cookie object.
2: HttpCookie cookie = new HttpCookie("Preferences");
3: // Set a value in it.
4: cookie["LanguagePref"] = "English";
5: // Add another value.
6: cookie["Country"] = "US";
7: // Add it to the current web response.
8: Response.Cookies.Add(cookie);
A cookie added in this way will persist until the user closes the browser and will be sent with every request. To create a longer-lived cookie, you can set an expiration date, as shown here:
1: // This cookie lives for one year.
2: cookie.Expires = DateTime.Now.AddYears(1);
Cookies are retrieved by cookie name using the Request.Cookies collection, as shown here:
1: HttpCookie cookie = Request.Cookies["Preferences"];
2: // Check to see whether a cookie was found with this name.
3: // This is a good precaution to take,
4: // because the user could disable cookies,
5: // in which case the cookie would not exist.
6: string language;
7: if (cookie != null)
8: {
9: language = cookie["LanguagePref"];
10: }
The only way to remove a cookie is by replacing it with a cookie that has an expiration date that has already passed. The following code demonstrates this technique:
1: HttpCookie cookie = new HttpCookie("LanguagePref");
2: cookie.Expires = DateTime.Now.AddDays(-1);
3: Response.Cookies.Add(cookie);
■Note
You’ll find that some other ASP.NET features use cookies. Two examples are session state (which allows you to temporarily store user-specific information in server memory) and forms security (which allows you to restrict
portions of a website and force users to access it through a login page).
Session State
Session state is the heavyweight of state management. It allows information to be stored in one page and accessed in another, and it supports any type of object, including your own custom data types.
Best of all, session state uses the same collection syntax as view state. The only difference is the name
of the built-in page property, which is Session.
1. Session Architecture
Session management is not part of the HTTP standard. As a result, ASP.NET needs to do some extra work to track session information and bind it to the appropriate response.
ASP.NET tracks each session using a unique 120-bit identifier. ASP.NET uses a proprietary algorithm to generate this value, thereby guaranteeing (statistically speaking) that the number is unique and that it’s random enough so a malicious user can’t reverse-engineer or guess what session ID a given client will be using.
This ID is the only piece of information that is transmitted between the web server and the client. When the client presents the session ID, ASP.NET looks up the corresponding session, retrieves the serialized data from the state server, converts it to live objects, and places these objects into a special collection so they can be accessed in code. This process takes place automatically.
■Note Every time you make a new request, ASP.NET generates a new session ID until you actually use session
state to store some information. This behavior achieves a slight performance enhancement—in short, why bother
to save the session ID if it’s not being used?
At this point you’re probably wondering where ASP.NET stores session information and how it serializes and deserializes it. In classic ASP, the session state is implemented as a free-threaded COM object that’s contained in the asp.dll library.
In ASP.NET, the programming interface is nearly identical, but the underlying implementation is quite a bit different.
When ASP.NET handles an HTTP request, it flows through a pipeline of different modules that can react to application events. One of the modules in this chain is the SessionStateModule (in the System.Web.SessionState namespace). The SessionStateModule generates the session ID, retrieves the session data from external state providers, and binds the data to the call context of the request. It also saves the session state information when the page is finished processing.
However, it’s important to realize that the SessionStateModule doesn’t actually store the session data. Instead, the session state is persisted in external components, which are named state providers. Figure 6-5 shows this interaction.
Session state is another example of ASP.NET’s pluggable architecture. A state provider is any class that implements the IHttpSessionState interface, which means you can customize how session state works simply by building (or purchasing) a new .NET component. ASP.NET includes three prebuilt state providers, which allow you to store information in process, in a separate service, or in a SQL Server database.
For session state to work, the client needs to present the appropriate session ID with each request. The final ingredient in the puzzle is how the session ID is tracked from one request to the next. You can accomplish this in two ways:
- Using cookies: In this case, the session ID is transmitted in a special cookie (named ASP.NET_SessionId), which
- ASP.NET creates automatically when the session collection is used. This is the default, and it’s also the same approach that was used in earlier versions of ASP.
Using modified URLs: In this case, the session ID is transmitted in a specially modified (or “munged”) URL. This- allows you to create applications that use session state with clients that don’t support cookies.
2. Using Session State
Session state is global to your entire application for the current user. Session state can be lost in
several ways:
• If the user closes and restarts the browser.
• If the user accesses the same page through a different browser window, although the session will still exist if a web page is accessed through the original browser window. Browsers differ on how they handle this situation.
• If the session times out because of inactivity. By default, a session times out after 20 idle minutes.
• If the programmer ends the session by calling Session.Abandon().
In the first two cases, the session actually remains in memory on the server, because the web server has no idea that the client has closed the browser or changed windows. The session will linger in memory, remaining inaccessible, until it eventually expires.
In addition, session state will be lost when the application domain is re-created. This process happens transparently when you update your web application or change a configuration setting.
The application domain may also be recycled periodically to ensure application health. If this behavior is causing a problem, you can store session state information out of process, as described in the next section. With out-of-process state storage, the session information is retained even when the application domain is shut down.
3. Securing Session State
The information in session state is very secure, because it is stored exclusively on the server. However, the cookie with the session ID can easily become compromised.
Several workarounds address this problem. One common approach is to use a custom session module that checks for changes in the client’s IP address
(see http://msdn.microsoft.com/msdnmag/issues/04/08/WickedCode for a sample implementation).
However, the only truly secure approach is to restrict session cookies to portions of your website that use SSL. That way, the session cookie is encrypted and useless on other computers.
If you choose to use this approach, it also makes sense to mark the session cookie as a secure cookie so that it will be sent only over SSL connections. That prevents the user from changing the URL from https:// to http://, which would send the cookie without SSL. Here’s the code you need:
1: Request.Cookies["ASP.NET_SessionId"].Secure = true;
Typically, you’ll use this code immediately after the user is authenticated. Make sure there is at least one piece of information in session state so the session isn’t abandoned (and then re-created later).
When using cookieless sessions, always set regenerateExpiredSessionId to true. This prevents the attacker from supplying a session ID that’s expired. Next, explicitly abandon the current session before logging in a new user.
Application State
In fact, application state is rarely used in the .NET world because its two most common uses have been replaced by easier, more efficient methods:
• In the past, application state was used to store application-wide constants, such as a database connection string. As you saw in Chapter 5, this type of constant can now be stored in the web.config file, which is generally more flexible because you can change it easily without needing to hunt through web-page code or recompile your application.
• Application state can also be used to store frequently used information that is time-consuming to create, such as a full product catalog that requires a database lookup. However, using application state to store this kind of information raises all sorts of problems about how to check if the data is valid and how to replace it when needed. It can also hamper performance if the product catalog is too large. A similar but much more sensible approach is to store frequently used information in the ASP.NET cache. Many uses of application state can be replaced more efficiently with caching.
Application state information is always stored in process. This means you can use any .NET data types. However, it also introduces the same two limitations that affect in-process session state.
Namely, you can’t share application state between the servers in a web farm, and you will always lose your application state information when the application domain is restarted—an event that can occur as part of ASP.NET’s normal housekeeping.
Static Application Variables
You can add static member variables to the global.asax file. These members are then compiled into the custom HttpApplication class for your web application and made available to all pages.
Here’s an example that declares a static array of strings:
public static string[] fileList;
The key detail that allows this to work is that the variable is static. That’s because ASP.NET creates a pool of HttpApplication classes to serve multiple requests. As a result, each request might be served with a different HttpApplication object, and each HttpApplication object has its own instance data. However, there is only one copy of the static data, which is shared for all instances (on the same web server).
There’s another requirement to make this strategy work. The rest of your code needs to be able to access the static members you’ve added to your custom application class. To make this possible, you need to specify the name that should be used for that class. To do this, you set the ClassName property of the Application directive, which is at the start of the global.asax file. Here’s an example that gives the application class the name Global:
1: <%@ Application Language="C#" ClassName="Global" %>
Now you can write code like this in your web pages:
string firstEntry = Global.fileList[0];
Using static member variables instead of the Application collection has two advantages. First, it allows you to write custom code that runs automatically when the value is accessed or changed (by wrapping your data in property procedures or methods). You could use this code to log how many times a value is being accessed, to check if the data is still valid, or to re-create it.
The other benefit of using static member variables is that the code that consumes them can be typesafe.
--------------------------------------
Regards,
FangwenYu