Improving ASP.NET Performance Part 2: Design Considerations

In our previous series of articles we introduced our new series about ASP.NET performance. In this second article in the series we’ll discuss Design Considerations.

Building high-performance ASP.NET applications is significantly easier if you design with performance in mind. Make sure you develop a performance plan from the outset of your project and don’t try to add performance as a post-build step.

We’ll discuss some best practice design guidelines that can sinificantly increase your success with creating a high-performance Web application.

Consider Security and Performance

Your choice of authentication scheme can affect the performance and scalability of your application. You need to consider the following issues:

  • Identities. Consider the identities you are using and the way that you flow identity through your application. To access downstream resources, you can use the ASP.NET process identity or another specific service identity. Or, you can enable impersonation and flow the identity of the original caller. If you connect to Microsoft SQL Server, you can also use SQL authentication. When you connect to a shared resource, such as a database, by using a single identity, you benefit from connection pooling. Connection pooling significantly increases scalability. If you flow the identity of the original caller by using impersonation, you cannot benefit from efficient connection pooling, and you have to configure access control for multiple individual user accounts. For these reasons, it is best to use a single trusted identity to connect to downstream databases.
  • Managing credentials. Consider the way that you manage credentials. You have to decide if your application stores and verifies credentials in a database, or if you want to use an authentication mechanism provided by the operating system where credentials are stored for you in Active Directory.

It is also a good idea to determine the number of concurrent users that your application can support and determine the number of users that your credential store (database or Active Directory) can handle. Perform capacity planning for your application to determine if the system can handle the anticipated load.

  • Protecting credentials. Your decision to encrypt and decrypt credentials when they are sent over the network costs additional processing cycles. If you use Windows Forms authentication or SQL authentication, credentials will flow in clear text and can be accessed by network eavesdroppers. Decide if you can choose authentication schemes that are provided by the operating system, such as NTLM or the Kerberos protocol, where credentials are not sent over the network to avoid encryption overhead.
  • Cryptography. If you only need to ensure that information is not tampered with during transmission, you can use keyed hashing. Encryption is not required in this case. If you need to hide the data that you send over the network, you require encryption and probably keyed hashing to ensure data validity. If both parties can share the keys, symmetric encryption offers better performance in comparison to asymmetric encryption. Although larger key sizes provide greater encryption strength, performance is slower relative to smaller key sizes. You must consider this type of performance and balance the larger key sizes against security tradeoffs at design time.

Partition Your Application Logically

Use layering to logically partition your application logic into presentation, business, and data access layers. This helps you create maintainable code, but it also permits you to monitor and optimize the performance of each layer separately. A clear logical separation also offers more choices for scaling your application. Try to reduce the amount of code in your code-behind files to improve maintenance and scalability.

Don’t confuse logical partitioning with physical deployment. A logical separation enables you to decide whether to locate presentation and business logic on the same server and clone the logic across servers in a Web farm, or to decide to install the logic on servers that are physically separate. The key point to remember is that remote calls incur a latency cost, and that latency increases as the distance between the layers increases.

Evaluate Affinity

Affinity can improve performance but it may affect you’re the scalability of your application. Common coding practices that use resource affinity are the following:

  • Using in-process session state. To avoid server affinity, maintain ASP.NET session state out of process in a SQL Server database or use the out-of-process state service running on a remote machine. Alternatively, design a stateless application, or store state on the client and pass it with each request.
  • Using computer-specific encryption keys. Using computer-specific encryption keys to encrypt data in a database prevents your application from working in a Web farm because common encrypted data needs to be accessed by multiple Web servers. A better approach is to use computer-specific keys to encrypt a shared symmetric key. You use the shared symmetric key to store encrypted data in the database.

Reduce Round Trips

Use the following techniques and features in ASP.NET to minimize the number of round trips between a Web server and a browser, and between a Web server and a downstream system:

  • HttpResponse.IsClientConnected. Consider using the HttpResponse.IsClientConnected property to verify if the client is still connected before processing a request and performing expensive server-side operations. However, this call may need to go out of process on IIS 5.0 and can be very expensive. If you use it, measure whether it actually benefits your scenario.
  • Caching. If your application is fetching, transforming, and rendering data that is static or nearly static, you can avoid redundant hits by using caching.
  • Output buffering. Reduce roundtrips when possible by buffering your output. This approach batches work on the server and avoids chatty communication with the client. The downside is that the client does not see any rendering of the page until it is complete. You can use the Response.Flush method. This method sends output up to that point to the client. The response time of your server is affected because your server needs to wait for acknowledgements from the client. The acknowledgements from the client occur after the client receives all the content from the server.
  • Server.Transfer. Where possible, use the Server.Transfer method instead of the Response.Redirect method. Response.Redirect sends a response header to the client that causes the client to send a new request to the redirected server by using the new URL. Server.Transfer avoids this level of indirection by simply making a server-side call.

You cannot always just replace Response.Redirect with Server.Transfer calls because Server.Transfer uses a new handler during the handler phase of request processing. If you need authentication and authorization checks during redirection, use Response.Redirect instead of Server.Transfer because the two mechanisms are not equivalent. When you use Response.Redirect, ensure you use the overloaded method that accepts a Boolean second parameter, and pass a value of false to ensure an internal exception is not raised.

Also note that you can only use Server.Transfer to transfer control to pages in the same application. To transfer to pages in other applications, you must use Response.Redirect.

Avoid Blocking on Long-Running Tasks

If you run long-running or blocking operations, consider using the following asynchronous mechanisms to free the Web server to process other incoming requests:

  • Use asynchronous calls to invoke Web services or remote objects when there is an opportunity to perform additional parallel processing while the Web service call proceeds. Where possible, avoid synchronous (blocking) calls to Web services because outgoing Web service calls are made by using threads from the ASP.NET thread pool. Blocking calls reduce the number of available threads for processing other incoming requests.
  • Consider using the OneWay attribute on Web methods or remote object methods if you do not need a response. This “fire and forget” model allows the Web server to make the call and continue processing immediately. This choice may be an appropriate design choice for some scenarios.
  • Queue work, and then poll for completion from the client. This permits the Web server to invoke code and then let the Web client poll the server to confirm that the work is complete.

Use Caching

A good caching strategy is typically the single most important performance-related design consideration. ASP.NET caching features include output caching, partial page caching, and the cache API.

Caching can be used to reduce the cost of data access and rendering output. Knowing how your pages use or render data enables you to design efficient caching strategies. Caching is particularly useful when your Web application constantly relies on data from remote resources such as databases, Web services, remote application servers, and other remote resources. Applications that are database intensive can benefit from caching by reducing the load on the database and by increasing the throughput of the application. Commonly, if caching is cheaper than the equivalent processing, you should use caching. Consider the following when you design for caching:

  • Identify data or output that is expensive to create or retrieve. Caching data or output that is expensive to create or retrieve can reduce the costs of obtaining the data. Caching the data reduces the load on your database server.
  • Evaluate the volatility. For caching to be effective, the data or output should be static or infrequently modified. Lists of countries, states, or zip codes are some simple examples of the type of data that you might want to cache. Data or output that changes frequently is usually less suited to caching but can be manageable, depending upon the need. Caching user data is typically only recommended when you use specialized caches, such as the ASP.NET session state store.
  • Evaluate the frequency of use. Caching data or output that is frequently used can provide significant performance and scalability benefits. You can obtain performance and scalability benefits when you cache static or frequently modified data and output alike. For example, frequently used, expensive data that is modified on a periodic basis may still provide large performance and scalability improvements when managed correctly. If the data is used more often than it is updated, the data is a candidate for caching.
  • Separate volatile data from nonvolatile data. Design user controls to encapsulate static content such as navigational aids or help systems, and keep them separate from more volatile data. This permits them to be cached. Caching this data decreases the load on your server.
  • Choose the right caching mechanism. There are many different ways to cache data. Depending on your scenario, some are better than others. User-specific data is typically stored in the Session object. Static pages and some types of dynamic pages such as non-personalized pages that are served to large user sets can be cached by using the ASP.NET output cache and response caching. Static content in pages can be cached by using a combination of the output cache and user controls. The ASP.NET caching features provide a built-in mechanism to update the cache. Application state, session state, and other caching means do not provide a built-in mechanism to update the cache.

Avoid Unnecessary Exceptions

Exceptions add significant overhead to your application. Design your code to avoid exceptions where possible.  If your application does not handle an exception, it ultimately handled by the ASP.NET exception handler. When designing your exception handling strategy, consider the following:

  • Design code to avoid exceptions. Validate user input and check for known conditions that can cause exceptions. Design code to avoid exceptions.
  • Avoid using exceptions to control logic flow. Avoid using exception management to control regular application logic flow.
  • Avoid relying on global handlers for all exceptions. Exceptions cause the runtime to manipulate and walk the stack. The further the runtime traverses the stack searching for an exception handler, the more expensive the exception is to process.
  • Catch and handle exceptions close to where they occur. When possible, catch and handle exceptions close to where they occur. This avoids excessive and expensive stack traversal and manipulation.
  • Do not catch exceptions you cannot handle. If your code cannot handle an exception, use a try/finally block to ensure that you close resources, regardless of whether an exception occurs. When you use a try/finally block, your resources are cleaned up in the finally block if an exception occurs, and the exception is permitted to propagate up to an appropriate handler.
  • Fail early to avoid expensive work. Design your code to avoid expensive or long-running work if a dependent task fails.
  • Log exception details for administrators. Implement an exception logging mechanism that captures detailed information about exceptions so that administrators and developers can identify and remedy any issues.
  • Avoid showing too much exception detail to users. Avoid displaying detailed exception information to users, to help maintain security and to reduce the amount of data that is sent to the client.

In this article we’ve covered the most important design considerations that affect the performance of your ASP.NET application. In our next article, we’ll discuss Implementation Considerations.