One of the reasons for ASP.NET's success is that it lowers the barrier to entry for Web developers. You don't have to have a PhD in computer science to write ASP.NET code. Many ASP.NET developers I meet at work are self-taught and were writing Microsoft® Excel® spreadsheets before they were writing C# or Visual Basic®. Now, they're writing Web applications, and overall, they deserve praise for the work they're doing.
But with power comes responsibility, and even experienced ASP.NET developers can make mistakes. In many years of consulting on ASP.NET projects, I've discovered that certain errors are particularly likely to lead to recurring defects. Some of these errors can impact performance. Other errors can inhibit scalability. Some bugs can also cost development teams valuable time tracking down bugs and unexpected behavior.
Here are 10 pitfalls that can cause problems during the release of ASP.NET production applications and ways to avoid them. All examples come from my own experience building real Web applications at real companies, and in some cases I provide context by describing some of the problems that the ASP.NET development team encountered during the development process.
LoadControl and Output Caching There are very few ASP.NET applications that do not use user controls. Before master pages, developers used user controls to extract common content, such as headers and footers. Even in ASP.NET 2.0, user controls provide an efficient way to encapsulate content and behavior and to divide the page into regions whose cacheability can be controlled independently of the page as a whole (a process called segments). A special form of output caching).
User controls can be loaded declaratively or forcefully. Forced loading relies on Page.LoadControl, which instantiates a user control and returns a control reference. If the user control contains members of a custom type (for example, a public property), you can cast the reference and access the custom member from your code. The user control in Figure 1 implements a property named BackColor. The following code loads the user control and assigns a value to BackColor:
protected void Page_Load(object sender, EventArgs e){// Load the user control and add it to the page Control control = LoadControl("~/MyUserControl.ascx");PlaceHolder1 .Controls.Add(control);//Set its background color ((MyUserControl)control).BackColor = Color.Yellow;}
The above code is actually very simple, but it is a trap waiting for the unwary developer to fall into. Can you find the flaw?
If you guessed that the problem is related to output caching, you are correct. As you can see, the above code example compiles and runs fine, but if you try to add the following statement (which is perfectly legal) to MyUserControl.ascx:
<%@ OutputCache Duration="5" VaryByParam="None" %>
Then the next time you run the page, you will see an InvalidCastException (oh joy!) and the following error message:
"Cannot cast an object of type 'System.Web.UI.PartialCachingControl' to type 'MyUserControl'."
Therefore, this code runs fine without the OutputCache directive, but fails if the OutputCache directive is added. ASP.NET is not supposed to behave this way. Pages (and controls) should be agnostic to output caching. So, what does this mean?
The problem is that when output caching is enabled for a user control, LoadControl no longer returns a reference to the control instance; instead, it returns a reference to a PartialCachingControl instance, which may or may not wrap the control instance, depending on whether the control's output is cache. Therefore, if a developer calls LoadControl to dynamically load a user control and converts the control reference in order to access control-specific methods and properties, they must pay attention to how they do this so that the code will run regardless of whether there is an OutputCache directive.
Figure 2 illustrates the correct way to dynamically load a user control and convert the returned control reference. Here's a summary of how it works:
• If the ASCX file is missing an OutputCache directive, LoadControl returns a MyUserControl reference. Page_Load converts the reference to MyUserControl and sets the control's BackColor property.
• If the ASCX file includes an OutputCache directive and the control's output is not cached, LoadControl returns a reference to the PartialCachingControl whose CachedControl property contains a reference to the underlying MyUserControl. Page_Load converts PartialCachingControl.CachedControl to MyUserControl and sets the control's BackColor property.
• If the ASCX file includes an OutputCache directive and the control's output is cached, LoadControl returns a reference to the PartialCachingControl whose CachedControl property is empty. Note that Page_Load no longer proceeds. The control's BackColor property cannot be set because the control's output comes from the output cache. In other words, there is no MyUserControl to set properties on at all.
The code in Figure 2 will run regardless of whether there is an OutputCache directive in the .ascx file. Although it looks a little more complicated, it will avoid annoying mistakes. Simple doesn't always mean easy to maintain.
Back to Top Sessions and Output Caching Speaking of output caching, both ASP.NET 1.1 and ASP.NET 2.0 have a potential issue that affects output cache pages in servers running Windows Server™ 2003 and IIS 6.0. I have personally seen this problem occur twice in an ASP.NET production server, and both times it was resolved by turning off output buffering. Later I learned that there is a better solution than disabling output caching. Here's what it looked like when I first encountered this problem.
What happened was that a website (let's call it Contoso.com here, which runs a public e-commerce application in a small ASP.NET web realm) contacted my team complaining that they were experiencing "cross-threading" mistake. Customers using the Contoso.com website often suddenly lose data they have entered but instead see data related to another user. After a little analysis, we found that the description of cross-threading is not accurate; the "cross-session" error is more appropriate. It appears that Contoso.com stores data in session state, and for some reason users occasionally and randomly connect to other users' sessions.
One of my team members wrote a diagnostic tool that logs key elements of each HTTP request and response, including the Cookie header. He then installed the tool on Contoso.com's Web server and let it run for a few days. The results are very obvious. About once in every 100,000 requests, ASP.NET correctly assigns a session ID to a completely new session and returns the session ID in the Set-Cookie header. It then returns the same session ID (that is, the same Set-Cookie header) on the next immediately adjacent request, even though the request was already associated with a valid session and the session ID in the cookie was submitted correctly. In effect, ASP.NET randomly switches users out of their own sessions and connects them to other sessions.
We were surprised and set out to find out why. We first checked the source code of Contoso.com and, to our relief, the problem was not there. Next, to ensure that the problem was not related to the application host in the Web realm, we left only one server running and shut down all the others. The problem persists, which is not surprising since our logs show that matching Set-Cookie headers never come from two different servers. ASP.NET accidentally generates duplicate session IDs, which is incredible because it uses the .NET Framework RNGCryptoServiceProvider class to generate these IDs, and the session IDs are long enough to ensure that the same ID is never generated twice (at least on the next It will not be generated twice in trillions of years). Beyond that, even if the RNGCryptoServiceProvider mistakenly generates repeated random numbers, it doesn't explain why ASP.NET mysteriously replaces the valid session ID with a new one (which is not unique).
On a hunch, we decided to take a look at output caching. When the OutputCacheModule caches an HTTP response, it must be careful not to cache the Set-Cookie header; otherwise, a cached response containing a new session ID will connect all recipients of the cached response (and the user whose request generated the cached response) to the same session. We checked the source code; Contoso.com has output caching enabled in both pages. We turned off output caching. As a result, the application ran for several days without a single cross-session issue. After that, it ran without any errors for over two years. In another company with a different application and a different set of web servers, we saw the exact same problem disappear. Just like at Contoso.com, eliminating the output cache solves the problem.
Microsoft later confirmed that this behavior stems from a problem in the OutputCacheModule. (An update may have been released by the time you read this article.) When ASP.NET is used with IIS 6.0 and kernel-mode caching is enabled, the OutputCacheModule sometimes fails to remove the Set-Cookie header from cached responses that it passes to Http.sys . The following is the specific sequence of events that results in the error:
• A user who has not recently visited the site (and therefore does not have a corresponding session) requests a page that has output caching enabled, but whose output is not currently available in the cache.
• The request executes code that accesses the user's most recently created session, causing the session ID cookie to be returned in the Set-Cookie header of the response.
• OutputCacheModule provides output to Http.sys, but cannot remove the Set-Cookie header from the response.
• Http.sys returns cached responses on subsequent requests, mistakenly connecting other users to the session.
The moral of the story? Session state and kernel-mode output caching don't mix. If you use session state in a page with output caching enabled, and the application is running on IIS 6.0, you need to turn off kernel-mode output caching. You will still benefit from output caching, but because kernel-mode output caching is much faster than normal output caching, the caching will not be as efficient. For more information about this issue, see support.microsoft.com/kb/917072.
You can turn off kernel-mode output caching for an individual page by including the VaryByParam="*" attribute in the page's OutputCache directive, although doing so may result in a sudden increase in memory requirements. Another, more secure approach is to turn off kernel-mode caching for the entire application by including the following element in web.config:
<httpRuntime enableKernelOutputCache="false" />
You can also use a registry setting to disable kernel-mode output caching globally, that is, disable kernel-mode output caching for all servers. See support.microsoft.com/kb/820129 for details.
Every time I hear a customer report puzzling session issues, I ask them if they are using output caching on any pages. If they do use output caching, and the host OS is Windows Server 2003, I would recommend that they disable kernel mode output caching. The problem is usually solved. If the problem is not resolved, the bug exists in the code. Be alert!
Return to top
Forms Authentication Ticket Lifetime Can you identify the problem with the following code?
FormsAuthentication.RedirectFromLoginPage(username, true);
This code may appear to be fine, but should never be used in an ASP.NET 1.x application unless code elsewhere in the application offsets the negative effects of this statement. If you're not sure why, keep reading.
FormsAuthentication.RedirectFromLoginPage performs two tasks. First, when FormsAuthenticationModule redirects the user to the login page, FormsAuthentication.RedirectFromLoginPage redirects the user to the page they originally requested. Second, it issues an authentication ticket (usually carried in a cookie, and always carried in a cookie in ASP.NET 1.x) that allows the user to remain authenticated for a predetermined period of time.
The problem lies in this time period. In ASP.NET 1.x, passing another parameter that is false to RedirectFromLoginPage issues a temporary authentication ticket that expires after 30 minutes by default. (You can change the timeout period using the Timeout attribute in the element of web.config.) However, passing another parameter of true will issue a permanent authentication ticket that is valid for 50 years! This creates a problem because If someone steals that authentication ticket, they can use the victim's identity to access the website for the duration of the ticket. There are many ways to steal authentication tickets — probing unencrypted traffic on public wireless access points, scripting across websites, gaining physical access to the victim's computer, etc. — so passing true to RedirectFromLoginPage is more secure than disabling your website Not much better. Fortunately, this problem has been resolved in ASP.NET 2.0. RedirectFromLoginPage now accepts the timeouts specified in web.config for temporary and permanent authentication tickets in the same way.
One solution is to never pass true in the second parameter of RedirectFromLoginPage in ASP.NET 1.x applications. But this is impractical because login pages often feature a "Keep me logged in" box that the user can check to receive a permanent rather than a temporary authentication cookie. Another solution is to use the code snippet in Global.asax (or the HTTP module if you prefer), which modifies the cookie containing the permanent authentication ticket before it is returned to the browser.
Figure 3 contains one such code snippet. If this code snippet is in Global.asax, it modifies the Expires property of the outgoing permanent Forms authentication cookie so that the cookie expires after 24 hours. You can set the timeout to any date you like by modifying the line commented "New expiration date".
You may find it strange that the Application_EndRequest method calls a local Helper method (GetCookieFromResponse) to check the authentication cookie for the outgoing response. The Helper method is a workaround for another bug in ASP.NET 1.1 that caused spurious cookies to be added to the response if you used the HttpCookieCollection's string index generator to check for non-existent cookies. Using an integer index generator as GetCookieFromResponse solves the problem.
Back to Top View State: The Silent Performance Killer In a sense, view state is the greatest thing ever. After all, view state enables pages and controls to maintain state between postbacks. Therefore, you don't have to write code to prevent the text in a text box from disappearing when a button is clicked, or to requery the database and rebind the DataGrid after a postback, as you would in traditional ASP.
But view state has a downside: When it grows too large, it becomes a silent performance killer. Some controls, such as text boxes, make decisions based on view state. Other controls (notably the DataGrid and GridView) determine their view state based on the amount of information displayed. I would be daunted if a GridView displayed 200 or 300 rows of data. Even though ASP.NET 2.0 view state is roughly half the size of ASP.NET 1.x view state, a bad GridView can easily reduce the effective bandwidth of the connection between the browser and the Web server by 50% or more.
You can turn off view state for individual controls by setting EnableViewState to false, but some controls (especially the DataGrid) lose some functionality when they can't use view state. A better solution for controlling view state is to keep it on the server. In ASP.NET 1.x, you could override the page's LoadPageStateFromPersistenceMedium and SavePageStateToPersistenceMedium methods and handle view state the way you liked. The code in Figure 4 shows an override that prevents view state from being retained in hidden fields and instead retains it in session state. Storing view state in session state is particularly effective when used with the default session state process model (that is, when session state is stored in an ASP.NET worker process in memory). In contrast, if session state is stored in the database, only testing can show whether retaining view state in session state improves or decreases performance.
The same approach is used in ASP.NET 2.0, but ASP.NET 2.0 provides an easier way to persist view state in session state. First, define a custom page adapter whose GetStatePersister method returns an instance of the .NET Framework SessionPageStatePersister class:
public class SessionPageStateAdapter :System.Web.UI.Adapters.PageAdapter{public override PageStatePersister GetStatePersister () {return new SessionPageStatePersister(this.Page ); }}
Then, register the custom page adapter as the default page adapter by placing the App.browsers file into your application's App_Browsers folder as follows:
<browsers><browser refID="Default"><controlAdapters><adapter controlType=" System.Web.UI.Page"adapterType="SessionPageStateAdapter" /></controlAdapters></browser></browsers>
(You can name the file anything you like, as long as it has a .browsers extension.) Afterwards, ASP.NET loads the page adapter and uses the returned SessionPageStatePersister to preserve all page state, including view state.
One disadvantage of using a custom page adapter is that it applies globally to every page in the application. If you prefer to keep the view state of some pages in session state but not others, use the method shown in Figure 4. Also, you may run into problems using this method if the user creates multiple browser windows in the same session.
Back to top
SQL Server session state: Another performance killer
ASP.NET makes it easy to store session state in the database: just toggle a switch in web.config and the session state is easily moved to the backend database. This is an important feature for applications running in the Web realm because it allows every server in the realm to share a common repository of session state. The added database activity reduces the performance of individual requests, but the increased scalability makes up for the loss in performance.
This all sounds good, but things change when you consider a few points:
• Even in applications that use session state, most pages don't use session state.
• By default, the ASP.NET session state manager performs two accesses (one read access and one write access) to the session data store in each request, regardless of whether the requested page uses session state.
In other words, when you use the SQL Server™ session state option, you pay a price (two database accesses) on every request—even on requests for pages that have nothing to do with session state. This has a direct negative impact on the throughput of the entire website.
Figure 5 Eliminate unnecessary session state database access
So what should you do? It's simple: disable session state in pages that don't use session state. This is always a good idea, but it's especially important when session state is stored in a database. Figure 5 shows how to disable session state. If the page does not use session state at all, include EnableSessionState="false" in its Page directive, like this:
<%@ Page EnableSessionState="false" ... %>
This directive prevents the session state manager from reading and writing to the session state database on every request. If the page reads data from session state but does not write data (that is, does not modify the contents of the user's session), set EnableSessionState to ReadOnly as follows:
<%@ Page EnableSessionState="ReadOnly" ... %>
Finally, if the page requires read/write access to session state, omit the EnableSessionState property or set it to true:
<%@ Page EnableSessionState="true" ... %>
By controlling session state in this way, you ensure that ASP.NET only accesses the session state database when really needed. Eliminating unnecessary database access is the first step in building high-performance applications.
By the way, the EnableSessionState property is public. This property has been documented since ASP.NET 1.0, but I still rarely see developers taking advantage of it. Maybe because it's not very important for the default session state model in memory. But it is important for the SQL Server model.
Back to Top Uncached Roles The following statement often appears in the web.config file of an ASP.NET 2.0 application and in the examples that introduce the ASP.NET 2.0 role manager:
<roleManager enabled="true" />
But as shown above, this statement does have a significant negative impact on performance. Do you know why?
By default, the ASP.NET 2.0 role manager does not cache role data. Instead, it consults the role data store each time it needs to determine which role, if any, the user belongs to. This means that once a user is authenticated, any pages that leverage role data (for example, pages that use sitemaps with security clipping settings enabled, and pages that have restricted access using role-based URL directives in web.config) will Causes the role manager to query the role data store. If roles are stored in a database, you can easily dispense with accessing multiple databases for each request. The solution is to configure the role manager to cache role data in cookies:
<roleManager enabled="true" cacheRolesInCookie="true" />
You can use other <roleManager> attributes to control the characteristics of the role cookie—for example, how long the cookie should remain valid (and therefore how often the role manager returns to the role database). Role cookies are signed and encrypted by default, so the security risk, while not zero, is mitigated.
Return to topConfiguration file property serialization
The ASP.NET 2.0 Profile Service provides a ready-made solution to the problem of maintaining per-user state, such as personalization preferences and language preferences. To use the profile service, you define an XML profile that contains the attributes that you want to preserve on behalf of an individual user. ASP.NET then compiles a class that contains the same properties and provides strongly typed access to class instances through configuration file properties added to the page.
Profile flexibility is so great that it even allows custom data types to be used as profile properties. However, there is a problem that I have personally seen cause developers to make mistakes. Figure 6 contains a simple class named Posts and a profile definition that uses Posts as a profile attribute. However, this class and this configuration file produce unexpected behavior at runtime. Can you figure out why?
The problem is that Posts contains a private field called _count, which must be serialized and deserialized in order to fully freeze and refreeze the class instance. However, _count is not serialized and deserialized because it is private and the ASP.NET Profile Manager uses XML serialization by default to serialize and deserialize custom types. The XML serializer ignores non-public members. Therefore, instances of Posts are serialized and deserialized, but each time a class instance is deserialized, _count is reset to 0.
One solution is to make _count a public field instead of a private field. Another solution is to encapsulate _count with a public read/write property. The best solution is to mark Posts as serializable (using the SerializableAttribute) and configure the profile manager to use the .NET Framework binary serializer to serialize and deserialize class instances. This solution maintains the design of the class itself. Unlike XML serializers, binary serializers serialize fields regardless of whether they are accessible. Figure 7 shows the fixed version of the Posts class and highlights the changed accompanying profile definition.
One thing you should keep in mind is that if you are using a custom data type as a profile property and that data type has non-public data members that must be serialized in order to fully serialize an instance of the type, use serializeAs="Binary" in the property declaration properties and ensure that the type itself is serializable. Otherwise, full serialization won't happen, and you'll waste time trying to determine why the profile isn't working.
Back to Top Thread Pool Saturation I'm often very surprised by the actual number of ASP.NET pages I see when executing a database query and waiting 15 seconds or more for query results to be returned. (I also waited 15 minutes before seeing the results of my query!) Sometimes the delay is an unavoidable consequence of the large amount of data being returned; other times the delay is due to poor database design. But regardless of the reason, long database queries or any type of long I/O operations will cause throughput to decrease in ASP.NET applications.
I have described this issue in detail before, so I won’t go into too much detail here. Suffice it to say that ASP.NET relies on a limited thread pool to handle requests. If all threads are occupied waiting for a database query, Web service call, or other I/O operation to complete, they will be released when an operation is completed. Before a thread is issued, other requests must be queued and waiting. When requests are queued, performance drops dramatically. If the queue is full, ASP.NET causes subsequent requests to fail with an HTTP 503 error. This is not a situation we would like to see on a production application on a production Web server.
The solution is asynchronous pages, one of the best yet little-known features of ASP.NET 2.0. A request for an asynchronous page starts on a thread, but when it starts an I/O operation, it returns to that thread and ASP.NET's IAsyncResult interface. When the operation is complete, the request notifies ASP.NET through IAsyncResult, and ASP.NET pulls another thread from the pool and completes processing of the request. It is worth noting that when I/O operations occur, no thread pool threads are occupied. This can significantly improve throughput by preventing requests for other pages (pages that are not performing lengthy I/O operations) from waiting in the queue.
You can read all about asynchronous pages in the October 2005 issue of MSDN® Magazine. Any page that is I/O bound rather than machine bound and takes a long time to execute has a good chance of becoming an asynchronous page.
When I tell developers about asynchronous pages, they often respond with "That's great, but I don't need them in my application." To which I reply with "Do any of your pages need to query the database?" ? Are they calling web services? Have you checked the ASP.NET performance counters for statistics on queued requests and average wait times? Even though your application is running fine so far, as your client size grows, the Load may increase. "
In fact, the vast majority of real-world ASP.NET applications require asynchronous pages. Please remember this!
Back to Top Impersonation and ACL Authorization The following is a simple configuration directive, but it makes my eyes light up every time I see it in web.config:
<identity impersonate="true" />
This directive enables client-side impersonation in ASP.NET applications. It attaches an access token representing the client to the thread handling the request so that the security checks performed by the operating system are against the client identity rather than the worker process identity. ASP.NET applications rarely require mocking; my experience tells me that developers often enable mocking for the wrong reasons. Here's why.
Developers often enable impersonation in ASP.NET applications so that file system permissions can be used to restrict access to pages. If Bob does not have permission to view Salaries.aspx, the developer will enable impersonation so that Bob can be prevented from viewing Salaries.aspx by setting the access control list (ACL) to deny Bob read permission. But there is the following hidden danger: impersonation is unnecessary for ACL authorization. When you enable Windows Authentication in an ASP.NET application, ASP.NET automatically checks the ACL for each .aspx page requested and denies requests from callers who do not have permission to read the file. It still behaves like this even if simulation is disabled.
Sometimes it is necessary to justify the simulation. But you can usually avoid it with good design. For example, assume that Salaries.aspx queries a database for salary information that only managers know. With impersonation, you can use database permissions to deny non-managerial personnel the ability to query payroll data. Or you can ignore impersonation and limit access to the payroll data by setting an ACL for Salaries.aspx so that non-administrators do not have read access. The latter approach provides better performance because it avoids mocking entirely. It also eliminates unnecessary database access. Why is querying the database denied just for security reasons?
By the way, I once helped troubleshoot a legacy ASP application that was restarting periodically due to an unrestricted memory footprint. An inexperienced developer converted the target SELECT statement into SELECT * without considering that the table being queried contained images, which were large and numerous. The problem is exacerbated by an undetected memory leak. (My area of managed code!) An application that had been working fine for years suddenly stopped working because SELECT statements that used to return a kilobyte or two of data were now returning several megabytes. Add to this the problem of inadequate version control, and the life of a development team is going to have to be “hyperactive” — and by “hyperactive,” it’s like having to watch your kids play an annoying game while you’re going to bed at night. Boring football game.
In theory, traditional memory leaks cannot occur in ASP.NET applications composed entirely of managed code. But insufficient memory usage can impact performance by forcing garbage collection to occur more frequently. Even in ASP.NET applications, be wary of SELECT *!
Back to top Don't rely entirely on it — set up the database's configuration file!
As a consultant, I'm often asked why applications aren't performing as expected. Recently, someone asked my team why an ASP.NET application was only completing approximately 1/100 of the throughput (requests per second) required to request a document. The problems we've discovered before are unique to problems we've seen in Web applications that didn't work properly—and are lessons we should all take seriously.
We run SQL Server Profiler and monitor the interaction between this application and the back-end database. In a more extreme case, just a single button click caused more than 1,500 errors to occur in the database. You can't build high-performance applications that way. A good architecture always starts with a good database design. No matter how efficient your code is, it won't work if it's weighed down by a poorly written database.
Poor data access architecture usually results from one or more of the following:
• Poor database design (usually designed by developers, not database administrators).
• Use of DataSets and DataAdapters—especially DataAdapter.Update, which works well for Windows Forms applications and other rich clients, but is generally not ideal for Web applications.
• A poorly designed data access layer (DAL) that has poorly programmed calculations and consumes many CPU cycles to perform relatively simple operations.
The problem must be identified before it can be treated. The way to identify data access problems is to run SQL Server Profiler or an equivalent tool to see what is happening behind the scenes. Performance tuning is completed after checking the communication between the application and the database. Give it a try—you might be surprised by what you find.
Back to Top Conclusion Now you know some of the problems and their solutions that you might encounter when building an ASP.NET production application. The next step is to take a closer look at your own code and try to avoid some of the problems I've outlined here. ASP.NET may have lowered the barrier to entry for Web developers, but your applications have every reason to be flexible, stable, and efficient. Please consider this carefully to avoid beginner mistakes.
Figure 8 provides a short checklist you can use to avoid the pitfalls described in this article. You can create a similar security defect checklist. For example:
• Have you encrypted configuration sections that contain sensitive data?
• Are you checking and validating the input used in database operations and are you using HTML encoded input as output?
• Does your virtual directory contain files with unprotected expansion names?
If you value the integrity of websites, servers carrying websites, and back -end resources they depend, these problems are very important.
Jeff Prosise is an editor who has contributed to MSDN Magazine and the author of multiple books. These books include Programming Microsoft .NET (Microsoft Press, 2002). He is also the co -founder of software consulting and education company Wintellect.
From the July 2006 issue of MSDN Magazine.