Caching Architecture Guide for .NET Framework Applications
Understanding Caching Technologies
Summary: This chapter discusses the various caching technologies available in distributed .NET applications, and details the benefits and limitations of each method.
Chapter 1 introduced you to the different types of state that can be used within a distributed .NET-based application and provided an overview of why and where to cache some of this state. You also learned about some of the issues you must bear in mind when you design the caching policy for your applications.
This chapter describes the different caching technologies you can use when you build distributed enterprise applications using the Microsoft .NET Framework. Other technologies can be used to cache data, such as NTFS file system caching, Active Directory® directory service caching, and the COM+ Shared Property Manager. However, in most cases, these methods are not recommended and are not described in this chapter.
This chapter describes the different caching technologies and Chapter 3, "Caching in Distributed Applications," describes how to select the right technology to fit your needs.
This chapter contains the following sections:
- "Using the ASP.NET Cache"
- "Using Remoting Singleton Caching"
- "Using Memory-Mapped Files"
- "Using Microsoft SQL Server 2000 or MSDE for Caching"
- "Using Static Variables for Caching"
- "Using ASP.NET Session State"
- "Using ASP.NET Client Side Caching and State"
- "Using Internet Explorer Caching"
Using the ASP.NET Cache
Storing frequently used data in memory is not a new concept for ASP developers. ASP offers the Session and Application objects, which enable storing key/value pairs in memory. The Session object is used to store per-user data across multiple requests, and the Application object is used to store per-application data for use by requests from multiple users.
ASP.NET introduces a new key/value pair object—the Cache object. The scope of the ASP.NET cache is the application domain; therefore, you cannot access it from other application domains or processes. The ASP.NET Cache object's life span is tied to the application, and the Cache object is re-created every time the application restarts, similar to the ASP Application object. The main difference between the Cache and Application objects is that the Cache object provides cache-specific features, such as dependencies and expiration policies.
You can programmatically access the Cache object and work with it using its properties and methods. This allows you full access to the object and the ability to use advanced caching features. You can also use the ASP.NET cache to store output responses that are transmitted to a Web browser.
Using Programmatic Caching
The Cache object is defined in the System.Web.Caching namespace. You can get a reference to the Cache object by using the Cache property of the HttpContext class in the System.Web namespace or by using the Cache property of the Page object. You can also access cached information in a user control through the Cache property of the UserControl class. This is useful particularly when you need to access the cache directly from a user control. In addition to simply storing key/value pairs, the Cache object provides additional functionality specifically designed to store transient data, such as .NET Framework objects. Cache features, such as dependencies and expiration policies, also extend the capabilities of the ASP.NET cache.
Using Dependencies and Expirations
When you add an item to the cache, you can define dependency relationships that can force that item to be removed from the cache under specific circumstances. The supported dependencies include the following:
File dependency—Allows you to invalidate a specific cache item when a disk-based file or files change.
The following example shows how to specify a file dependency when adding an item to the cache.
CacheDependency cDependency = new CacheDependency(Server.MapPath("authors.xml")); Cache.Insert("CachedItem", item, cDependency);
Key dependency—Invalidates a specific cache item when another cached item changes. For example, when you cache basic data alongside calculation results on that data, the calculated data should be invalidated when the basic data changes or becomes invalid. As another example, although you cannot directly remove a page from the ASP.NET output cache, you can use a key dependency on the page as a workaround. When the key on which your pages are dependent is removed from cache, your cached pages are removed as well.
The following example shows how to make one cache item dependent on another.
// Create a cache entry. Cache["key1"] = "Value 1"; // Make key2 dependent on key1. String[] dependencyKey = new String[1]; dependencyKey[0] = "key1"; CacheDependency dependency = new CacheDependency(null, dependencyKey); Cache.Insert("key2", "Value 2", dependency);
Time-based expiration—Invalidates a specific cached item at a predefined time. The time for invalidation can be absolute—such as Monday, December 1, at 18:00—or sliding, which resets the time to expire relative to the current time whenever the cached item is accessed. Examples of both types of time dependency are as follows.
/// Absolute expiration Cache.Insert("CachedItem", item, null, DateTime.Now.AddSeconds(5), Cache.NoSlidingExpiration); /// Sliding expiration Cache.Insert("CachedItem", item, null, Cache.NoAbsoluteExpiration, TimeSpan.FromSeconds(5));
Using a duration that's too short limits a cache's usefulness; using a duration that's too long may return stale data and unnecessarily load the cache with pages that are not requested. If using page output caching does not improve performance, your cache duration might be too short. Try fast concurrent page requests to see whether pages are served rapidly from cache.
If you do not have a mechanism, such as dependencies, to invalidate a cached page, keep the duration short. In general, keep your cache duration shorter than the update interval of the data in your page. Usually, if you receive pages that are out of date, your cache duration is too long.
A common question is how to implement a cache database dependency. This requires you to implement a notification mechanism that informs your cache of changes to the original data in the database. For information about using external notifications, see Chapter 5, "Managing the Contents of a Cache." For a downloadable code sample that shows how to programmatically implement database dependencies, see Rob Howard's team page.
When you need to access a cached item, you must check whether the item is in the cache before you try to use it. Because the ASP.NET cache invalidates cached items based on dependencies, time, or resources, you must always write code to create or retrieve the item you need if it does not currently exist in the cache.
The following example shows how you can do this.
string data = (string)Cache["MyItem"];
if (data == null)
{
data = GetData();
Cache.Insert("MyItem", data);
}
DoSomeThingWithData(data);
Check whether data in the cache is valid before using it to avoid using stale items. Use this method when the cost of validating a cached item is significantly less than the cost of regenerating or obtaining the data.
Items added to the cache automatically expire and are removed from the cache if they are not used for a given amount of time. Use the expiration cache feature when the data from which the cached items generate is updated at known intervals.
Dependencies and expirations initiate the removal of items from a cache. Sometimes you want to write code to be called when the removal occurs.
Using Cache Callbacks
You can add a callback method to a cached item to execute when that item is removed from the cache. You can implement such a callback to ensure that the cached item is not removed from the cache or that the cache is updated based on new data.
The following example shows how to define a callback function when adding an item to the cache.
CacheItemRemovedCallback onRemove = new
CacheItemRemovedCallback(this.RemovedCallback);
Cache.Insert("CachedItem",
item,
null,
Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.Default,
onRemove);
// Implement the function to handle the expiration of the cache.
public void RemovedCallback(string key, object value, CacheItemRemovedReason r)
{
// Test whether the item is expired, and reinsert it into the cache.
if (r == CacheItemRemovedReason.Expired)
{
// Reinsert it into the cache again.
CacheItemRemovedCallback onRemove = null;
onRemove = new CacheItemRemovedCallback(this.RemovedCallback);
Cache.Insert(key,
value,
null,
Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.Default,
onRemove);
}
}
Notice that in addition to expiration parameters, the Insert method also uses a CacheItemPriority enumeration.
Applying Priority to Cache Items
When the server running your ASP.NET application runs low on memory resources, items are removed from cache to free memory in a process known as scavenging. When memory is low, the cache determines which items are removed from cache based on priority. You can set the cache item priority when you add the item to the cache. Doing so controls which items scavenging removes first.
For more information about cache item priorities, see "CacheItemPriority Enumeration," in the MSDN Library.
Flushing a Cache
There is no direct support for automatic flushing of the ASP.NET output cache. One way to do so is to set an external dependency that clears all cached items automatically. For example, the following statement flushes the ASP.NET output cache as soon as it executes.
Response.Cache.SetExpires(DateTime.Now)
This code flushes the output cache, but the page does not reflect this until the original cache duration completes. For example, if you use the following directive to configure your cache, the cache resets after 10 seconds.
<%@ OutputCache Duration="10" VaryByParam="none" %>
Flushing the entire output cache is generally not required, and better alternatives are available to your application design. For example, when using the ASP.NET Cache object, you can reload your cache items when they become stale, overwriting the existing cache content.
Using an Output Cache
You can use two types of output caching to cache information that is to be transmitted to and displayed in a Web browser: page output caching and page fragment caching. Page output caching caches an entire Web page and is suitable only when the content of that page is fairly static. If parts of the page are changing, you can wrap the static sections as user controls and cache the user controls using page fragment caching.
Using Page Output Caching
Page output caching adds the response of a given request to the ASP.NET cache object. After the page is cached, future requests for that page return the cached information instead of re-creating the entire page. You can implement page output caching by adding the necessary page directives (high-level, declarative implementation) or by using the HTTPCachePolicy class in the System.Web namespace (low-level, programmatic implementation).
This guide does not discuss the technical details of how page output caching works but concentrates on guidelines and best practices of how to use it correctly. For more information about how to implement page output caching, see "Page Output Caching," in the MSDN Library.
Determining What to Cache with Page Output Caching
You can use the page output cache to cache a variety of information, including:
Static pages that do not change often and that are frequently requested by clients.
Pages with a known update frequency. For example, a page that displays a stock price where that price is updated at given intervals.
Pages that have several possible outputs based on HTTP parameters, and those possible outputs do not often change—for example, a page that displays weather data based on country and city parameters.
Results being returned from Web services. The following example shows how you can declaratively cache these results.
[WebMethod(CacheDuration=60)] public string HelloWorld() { return "Hello World"; }
Caching these types of output avoids the need to frequently process the same page or results.
Pages with content that varies—for example, based on HTTP parameters—are classed as dynamic, but they can still be stored in the page output cache. This is particularly useful when the range of outputs is small.
Caching Dynamic Pages
You may find yourself designing Web pages that contain dynamic content that is dependent upon input parameters, language, or browser type. ASP.NET lets you cache different versions of these pages based on changing data. By using applicable attributes on dynamically generated pages, you optimize cache usage and get better cache duration control.
You can use the following OutputCache attributes to implement this functionality:
- VaryByParam—Lets you cache different versions of the same page based on the input parameters sent through the HTTP GET/POST.
- VaryByHeader—Lets you cache different versions of the page based on the contents of the page header.
- VaryByCustom—Lets you customize the way the cache handles page variations by declaring the attribute and overriding the GetVaryByCustomString handler.
- VaryByControl—Lets you cache different versions of a user control based on the value of properties of ASP objects in the control.
The more specific you are in setting the values of these attributes—for example, supplying more of the HTTP parameters—the better the cache is used because it contains only relevant data. In essence, you are insulating what you are caching from changes in the underlying data. However, as the values become more specific, the cache uses memory less efficiently because the cache keeps more versions of the same page. The ASP.NET cache can erase pages by scavenging when memory becomes low.
Table 2.1 compares response times as the number of cached page versions increases.
Table 2.1: Caching performance
Pages cached | Responses/sec. | Time to first byte | Time to last byte |
---|---|---|---|
Less than 1,000 | 15.37 | 124.73 | 643.43 |
More than 1,200 | 3.15 | 2,773.2 | 3,153.63 |
Note These figures are based on sample pages of 250 KB.
For more information about the caching attributes in ASP.NET cache, see "Page Output Caching" and "Caching Multiple Versions of a Page," in the MSDN Library.
Configuring the Output Cache Location
You can control the location of your output cache by using the Location attribute of the @OutputCache directive. The Location attribute is supported only for page output caching and not for page fragment caching. You can use it to locate the cache on the originating server, the client, or a proxy server. For more information about the Location attribute, see "OutputCacheLocation Enumeration," in the MSDN Library.
The location of the cache is specified when you are configuring the cache for your application.
Configuring Page Output Caching
You can configure the cache both declaratively (using page directives) and programmatically (using the cache API). The declarative method can meet most of common requirements for configuring a cache, but the cache API includes some additional functionality, such as the ability to register callbacks.
The following example shows how to declaratively configure the cache using page directives in the ASP.NET page header.
<%@ OutputCache Duration="20" Location="Server" VaryByParam="state"
VaryByCustom="minorversion" VaryByHeader="Accept-Language"%>
The following example shows how to programmatically configure the cache using the cache API.
private void Page_Load(object sender, System.EventArgs e)
{
// Enable page output caching.
Response.Cache.SetCacheability(HttpCacheability.Server);
// Set the Duration parameter to 20 seconds.
Response.Cache.SetExpires(System.DateTime.Now.AddSeconds(20));
// Set the Header parameter.
Response.Cache.VaryByHeaders["Accept-Language"] = true;
// Set the cached parameter to 'state'.
Response.Cache.VaryByParams["state"] = true;
// Set the custom parameter to 'minorversion'.
Response.Cache.SetVaryByCustom("minorversion");
}
Both of these examples result in the same cache configuration.
Using Page Fragment Caching
Page fragment caching involves the caching of a fragment of the page, as opposed to the entire page. Sometimes full page output caching is not feasible—for example, when portions of the page need to be dynamically created for each user request. In such cases, it can be worthwhile to identify portions of the page or controls that do not often change and that take considerable time and server resources to create. After you identify these portions, you can wrap them in a Web Forms user control and cache the control so that these portions of the page don't need to be recreated each time.
You can implement page fragment caching by adding the necessary directives (high-level, declarative implementation) to the user control or by using metadata attributes in the user control class declaration.
For more information about page fragment caching, see "Caching Portions of an ASP.NET Page," in the MSDN Library.
Determining What to Cache with Page Fragment Caching
Use page fragment caching when you cannot cache the entire Web page. There are many situations that can benefit from page fragment caching, including:
- Page fragments (controls) that require high server resources to create.
- Sections on a page that contain static data.
- Page fragments that can be used more than once by multiple users.
- Page fragments that multiple pages share, such as menu systems.
Note ASP.NET version 1.1, part of the Microsoft Visual Studio® .NET 2003 development system, introduces a new Shared attribute in the user control's <%@ OutputCache %> directive. This attribute allows multiple pages to share a single instance of a cached user control. If you don't specify the Shared attribute, each page gets its own instance of the cached control.
Configuring Page Fragment Caching
The following example shows how to implement fragment caching in a Web user control.
// Partial caching for 120 seconds
[System.Web.UI.PartialCaching(120)]
public class WebUserControl : System.Web.UI.UserControl
{
// Your Web control code
}
When the page containing the user control is requested, only the user control—not the entire page—is cached.
Using the ASP.NET Cache in Non-Web Applications
The ASP.NET cache object is located in the System.Web namespace, and because it is a generic cache implementation, it can be used in any application that references this namespace.
The System.Web.Caching.Cache class is a cache of objects. It is accessed either through the static property System.Web.HttpRuntime.Cache or through the helper instance properties System.Web.UI.Page and System.Web.HttpContext.Cache. It is therefore available outside the context of a request. There is only one instance of this object throughout an entire application domain, so the HttpRuntime.Cache object can exist in each application domain outside of Aspnet_wp.exe.
The following code shows how you can access the ASP.NET cache object from a generic application.
HttpRuntime httpRT = new HttpRuntime();
Cache cache = HttpRuntime.Cache;
After you access the cache object for the current request, you can use its members in the usual way.
Managing the Cache Object
ASP.NET supports a host of application performance counters that you can use to monitor the cache object activity. Monitoring these performance counters can help you locate and resolve issues in the performance of your ASP.NET cache. Table 2.2 describes these counters.
Table 2.2: Application performance counters for monitoring a cache
Counter | Description |
---|---|
Cache Total Entries | The number of entries in the cache |
Cache Total Hits | The number of hits from the cache |
Cache Total Misses | The number of failed cache requests per application |
Cache Total Hit Ratio | The ratio of hits to misses for the cache |
Cache Total Turnover Rate | The number of additions and removals to the total cache per second |
Cache API Entries | The number of entries in the application (programmatic cache) |
Cache API Hits | The number of hits from the cache when the cache is accessed through the external cache APIs (programmatic cache) |
Cache API Misses | The number of failed requests to the cache when the cache is accessed through the external cache APIs (programmatic cache) |
Cache API Hit Ratio | The cache ratio of hits to misses when the cache is accessed through the external cache APIs (programmatic cache) |
Cache API Turnover Rate | The number of additions to and removals from the cache per second, when accessed through the external cache APIs (programmatic cache) |
Output Cache Entries | The number of entries in the output cache |
Output Cache Hits | The number of requests serviced from the output cache |
Output Cache Misses | The number of failed output cache requests per application |
Output Cache Hit Ratio | The percentage of requests serviced from the output cache |
Output Cache Turnover Rate | The number of additions to and removals from the output cache per second |
Note that the Cache API counters do not track internal usage of the cache by ASP.NET. The Cache Total counters track all cache usage.
The Turnover Rate counters help determine how effectively the cache is being used. If the turnover is large, the cache is not being used efficiently because cache items are frequently added and removed from the cache. You can also track cache effectiveness by monitoring the Hit counters.
For more information about these performance counters, see "Performance Counters for ASP.NET," in the MSDN Library.
Using Remoting Singleton Caching
Microsoft .NET remoting provides a rich and extensible framework for objects executing in different AppDomains, in different processes, and on different computers to communicate seamlessly with each other. Microsoft .NET remoting singleton objects service multiple clients and share data by storing state information between client invocations.
You can use .NET remoting when you need a custom cache that can be shared across processes in one or several computers. To do this, you must implement a caching service using a singleton object that serves multiple clients using .NET remoting.
To implement a cache mechanism using .NET remoting, ensure that the remote object lease does not expire and that the remoting object is not destroyed by the garbage collector. An object lease is the period of time that a particular object can remain in memory before the .NET remoting system begins to delete it and reclaim the memory. When implementing a remoting singleton cache, override the InitializeLifetimeService method of MarshalByRefObject to return null. Doing so ensures that the lease never times out and the object associated with it has an infinite lifetime. The following example shows how to cause your object to have infinite lifetime.
public class DatasetStore : MarshalByRefObject
{
// A hash table-based data store
private Hashtable htStore = new Hashtable();
//Returns a null lifetime manager so that GC won't collect the object
public override object InitializeLifetimeService() { return null; }
// Your custom cache interface
}
This code ensures that the garbage collector does not collect the object by returning a null lifetime manager for the object.
You can implement the remoting singleton as a caching mechanism in all layers of a multilayer architecture. However, because of the relatively high development cost of its implementation, you're most likely to choose this solution when a custom cache is needed in the application and data tiers.
You can often use solutions based on Microsoft SQL Server™ instead of remoting singleton caching solutions. It is tempting to choose remoting singleton caching because it is simple to implement, but the poor performance and the lack of scalability it produces mean that this choice is often misguided.
Using Memory-Mapped Files
Memory-mapped files offer a unique memory management feature that allows applications to use pointers to access files on disk—the same way that applications access dynamic memory. With this capability, you can map a view of all or part of a file on disk to a specific range of addresses in your process's address space. After you do so, accessing the content of a memory-mapped file is as simple as dereferencing a pointer in the designated range of addresses.
Both code and data are treated the same way in Windows: Both are represented by pages of memory, and both have those pages backed by a file on disk. The only difference is the type of file that backs them. Code is backed by the executable image, and data is backed by the system pagefile.
Because of the way that memory-mapped files function, they can also provide a mechanism for sharing data between processes. By extending the memory-mapped file capability to include portions of the system pagefile, applications are able to share data that is backed by that pagefile. Each application simply maps a view of the same portion of the pagefile, making the same pages of memory available to each application. Because all processes effectively share the same memory, application performance increases.
You can use these unique capabilities of memory-mapped files to develop an efficient custom cache that can be shared across multiple application domains and processes within the same computer. A custom cache based on memory-mapped files includes the following components:
- Windows NT service—Creates the memory-mapped file when it is started and deletes the memory-mapped file when it is stopped. Because each process using the cache needs a pointer to the memory-mapped file, you need a Windows NT® service to create the memory-mapped file and to pass handles to processes that want to use the cache. Alternatively, you can use a named memory-mapped file and get a handle to the memory-mapped file for each process by using the memory-mapped file name.
- Cache management DLL—Implements the specific cache functionality, such as:
- Inserting and removing items from the cache.
- Flushing the cache using algorithms such as Least Recently Used (LRU) and scavenging. For more information about flushing, see Chapter 5, "Managing the Contents of a Cache."
- Validating data to protect it against tampering or spoofing. For more information about security, see Chapter 6, "Understanding Advanced Caching Issues."
Because a memory-mapped file is a custom cache, it is not limited to a specific layer or technology in your application.
Memory-mapped file caches are not easy to implement because they require the use of complex Win32® application programming interface (API) calls. The .NET Framework does not support memory-mapped files, so any implementations of a memory-mapped file cache run as unmanaged code and do not benefit from any .NET Framework features, including memory management features, such as garbage collection, and security features, such as code access security.
Management functionality for memory-mapped file custom cache needs to be custom developed for your needs. Performance counters can be developed to show cache use, scavenging rate, hit-to-miss ratios, and so on.
Using Microsoft SQL Server 2000 or MSDE for Caching
You can use Microsoft SQL Server 2000, and its scaled down version, Microsoft SQL Server 2000 Desktop Engine (MSDE), to cache large amounts of data. Although SQL Server is not the ideal choice when caching small data items because of its relatively slow performance, it can be a powerful choice when data persistency is critical or when you need to cache a very large amount of data.
Note In caching terms, SQL Server 2000 and MSDE provide the same capabilities. All references to SQL Server in this section equally apply to MSDE.
If your application requires cached data to persist across process recycles, reboots, and power failures, in-memory cache is not an option. In such cases, you can use a caching mechanism based on a persistent data store, such as SQL Server or the NTFS file system. It also makes sense to use SQL Server to cache smaller data items to gain persistency.
SQL Server 2000 limits you to 8 KB of data when you are accessing VarChar and VarBinary fields using Transact-SQL or stored procedures. If you need to store larger items in your cache, use an ADO.NET SQLDataAdapter object to access DataSet and DataRow objects.
SQL Server caching is easy to implement by using ADO.NET and the .NET Framework, and it provides a common development model to use with your existing data access components. It provides a robust security model that includes user authentication and authorization and can easily be configured to work across a Web farm using SQL Server replication.
Because the cache service needs to access SQL Server over a network and the data is retrieved using database queries, the data access is relatively slow. Carefully compare the cost of recreating the data versus retrieving it from the database.
SQL Server caching can be useful when you require a persistent cache for large data items and you do not require very fast data retrieval. Carefully consider how much memory you have and how big your data items are. Do not use all of your memory to cache a few large data items because doing so degrades overall server performance. In such cases, caching items in SQL Server can be a good practice. Remember, though, that implementing a SQL Server caching mechanism requires installing, managing, and licensing at least one copy of SQL Server or MSDE.
When you use SQL Server for caching, you can use the SQL Server management tools, which include a host of performance counters that can be used to monitor cache activity. For example, use the SQLServer:Databases counters on your cache database table to monitor your cache performance, and use counters such as Transactions/sec and Data File(s) Size (KB) to monitor usage.
Using Static Variables for Caching
Static variables are often used to save class state, and you can use them to create customized cache objects. To do so, declare your cache data store as a static variable in your custom-developed cache class, and implement interfaces to insert, remove, and access items in the data store.
Using static variables is simple if no special cache capabilities are needed, such as dependencies, expiration, and scavenging. Because the cache is an in-memory system, static variables provide fast, direct access to the cached data. Static variable caching is useful to store state when other mechanisms are not available or are too costly. Use a static variable cache when you do not have another key/value dictionary available or when you need a fast in-process caching mechanism. In ASP.NET applications, use the Cache object instead.
You can use static variables to cache large data items, provided that the items do not often change. Because there is no mechanism for invalidating items, obsolete large items can waste valuable memory.
When using a static variable cache, you must ensure that you either implement your own thread safety mechanism or use a .NET Framework object that can be synchronized, for example, a Hashtable object.
The following example shows how to implement a static variable cache by using a hash table.
static Hashtable mCacheData = new Hashtable();
The scope of the static variables can be limited to a class or a module or be defined with project-level scope. If the variable is defined as public in your class, it can be accessed from anywhere in your project code, and the cache will be available throughout the application domain. The lifetime of static variables is directly linked to their scope.
Using ASP.NET Session State
You can use ASP.NET session state (based on the HttpSessionState class) to cache per-user session state. It solves many of the limitations that are inherent in ASP session state, including:
- The ASP session state is tied to the ASP hosting process, and as such is recycled when the ASP process is recycled. You can configure ASP.NET session state to avoid this.
- ASP session state has no solution for functioning in a Web server farm. The ASP.NET session state solution does not provide the best results when scalability and availability are important, but it works well for small scalar values, such as authentication information.
- ASP session state depends on the client browser accepting cookies. If cookies are disabled in the client browser, session state cannot be used. ASP.NET session state can be configured not to use cookies.
The ASP.NET session state is much improved. This section describes how and where session state is best used.
ASP.NET session state has three modes of operation:
- InProc—Session state is held in the memory space of the Aspnet_wp.exe process. This is the default setting. In this setting, the session state is recycled if the process or the application domain is recycled.
- StateServer—Session state is serialized and stored in a separate process (Aspnet_state.exe); therefore, the state can be stored on a separate computer (a state server).
- SQLServer—Session state is serialized and stored in a SQL Server database.
You can specify the mode to be used for your application by using the mode attribute of the <sessionState> element of the application configuration file.
The scope of the ASP.NET session state is limited to the user session it is created in. In out-of-process configurations—for example, using StateServer or SQLServer mode in a Web farm configuration—the session state can be shared across all servers in the farm. However, this benefit does come at a cost. Performance may decrease because ASP.NET needs to serialize and deserialize the data and because moving the data over the network takes time.
For more information about ASP.NET session state, see "ASP.NET Session State," in the MSDN Library.
Choosing the Session State Mode
Each mode of operation, InProc, StateServer, and SQLServer, presents different issues to consider when you design your caching policy.
Using InProc Mode
When you use InProc mode, you are storing the state in the Aspnet_wp.exe process. You cannot use InProc mode in a Web garden environment because multiple instances of Aspnet_wp.exe are running on the same computer.
InProc is the only mode that supports the Session_End event. The Session_End event fires when a user's session times out or ends. You can write code in this event to clean up server resources.
Using StateServer Mode
StateServer mode stores the state in a dedicated process. Because it is an out-of-process system, you must ensure that the objects you cache in it are serializable to enable cross-process transmission.
When using the Session object for caching in a Web farm environment, ensure that the <machineKey> element in Web.config is identical across all of your Web servers. Doing so guarantees that all computers are using the same encryption formats so that all computers can successfully access the stored data. For more information about encryption, see "<machineKey>Elementin the MSDN Library.
For session state to be maintained across servers in a Web farm, the application path of the Web site (for example, \LM\W3SVC\2) in the Internet Information Services (IIS) metabase must be identical across all servers in that farm. For more information about this, see article Q325056, "PRB: Session State Is Lost in Web Farm If You Use SqlServer or StateServer Session Mode," in the Microsoft Knowledge Base.
Using SQLServer Mode
In SQLServer mode, ASP.NET serializes the cache items and stores them in a SQL Server database, so you must ensure that the objects you are using are serializable.
If you specify integrated security in the connection string used to access your session state (for example, trusted_connection=true or integrated security=sspi), you cannot use impersonation in ASP.NET. For more information about this problem, see article Q324479, "FIX: ASP.NET SQL Server Session State Impersonation Is Lost Under Load," in the Microsoft Knowledge Base.
For session state to be maintained across different servers in a Web farm, the application path of the Web site (for example, \LM\W3SVC\2) in the IIS metabase should be identical across all servers in that farm. For details about how to implement this, see article Q325056, "Session State Is Lost in Web Farm If You Use SqlServer or StateServer Session Mode," in the Microsoft Knowledge Base.
You can configure ASP.NET session state to use SQL Server to store objects. Access to the session variables is slower in this situation than when you use an in-process cache; however, when session data persistency is critical, SQL Server provides a good solution. By default, SQL Server stores session state in the tempdb database, which is re-created after a SQL Server computer reboots. You can configure SQL Server to store session data outside of tempdb so that it is persisted even across reboots. For information about script files that you can use to configure SQL Server, see article Q311209, "Configure ASP.NET for Persistent SQL Server Session State Management," in the Microsoft Knowledge Base.
When you use session state in SQL Server mode, you can monitor the SQL Server tempdb counters to provide some level of performance monitoring.
Determining What to Cache in the Session Object
You can use the Session object to store any of the .NET Framework data types; however, be aware of which sort of data is best suited to each cache mode.
You can use any mode to store .NET Framework basic types (such as Int, Byte, String) because ASP.NET uses an optimized internal method for serializing and deserializing basic data types when using out-of-process modes.
Store complex types (such as ArrayList) only in the InProc mode, because ASP.NET uses the BinaryFormatter to serialize data, which can affect performance. Note that serialization occurs only in the StateServer and SQLServer out-of-process modes.
For example, the Session object is ideal for storing user authentication data during a user session. Because this information is likely to be stored as a .NET Framework basic type (such as, String), it can use any of the three modes. However, you must consider the security aspects of storing sensitive data in a cache. The data can be accessed from different pages without requiring the user to re-enter logon information.
Try to avoid using session state for large data items because this degrades your application performance. Table 2.3 compares response times as the size of the cache changes.
Table 2.3: Caching performance
Cache size | Responses per second | Time to first byte | Time to last byte |
---|---|---|---|
2 KB | 447.38 | 19.43 | 19.65 |
200 KB | 16.12 | 667.95 | 668.89 |
Because session state can function as both an in-process and an out-of-process cache, it is suitable for a wide range of solutions. It is simple to use, but it does not support data dependencies, expiration, or scavenging.
Implementing Session State
ASP.NET session state provides a simple interface for adding and removing variables and simple configuration using the Web.config file. Changes to this file are reflected immediately without the need to restart the ASP.NET process.
The following example shows how to implement SQLServer mode session caching in the Web.config file.
<sessionState
mode="SQLServer"
stateConnectionString="tcpip=127.0.0.1:42424"
sqlConnectionString="data source=127.0.0.1; Integrated Security=SSPI"
cookieless="false"
timeout="20"
/>
The following example shows how you can use the session object to store and access user credentials.
private void SaveSession(string CartID)
{
Session["ShoppingCartID"] = CartID;
}
private void CheckOut()
{
string CartID = (string)Session["ShoppingCartID"];
if(CartID != null)
{
// Transfer execution to payment page.
Server.Transfer("Payment.aspx");
}
else
{
// Display error message.
}
}
Remember to secure any credentials when caching them. For more information about securing custom caches, see "Securing a Custom Cache," in Chapter 6, "Understanding Advanced Caching Issues."
Using ASP.NET Client-Side Caching and State
You can reduce the workload on your server by storing page information using client-side options. This approach has minimal security support but results in faster server performance because the demand on server resources is modest. Because you must send information to the client before it can be stored, there is a practical limit on how much information you can store this way.
The main mechanisms to implement client-side caching are:
- Hidden fields
- View state
- Hidden frames
- Cookies
- Query strings
Each of these mechanisms is useful for caching different types of state in different scenarios.
Using Hidden Fields
You can store page-specific information in a hidden field on your page to maintain the state of that page. For more information about hidden fields, see "Introduction to Web Forms State Management," in the MSDN Library.
It is best to store only small amounts of frequently changing data in hidden fields because this data is included in the roundtrips to the server on every postback. Storing a large amount of data slows your application because of network traffic load. ASP.NET provides the HtmlInputHidden control, which offers hidden field functionality.
Note If you use hidden fields, you must submit your pages to the server by using the HTTP POST method rather than by requesting the page using the HTTP GET method.
Benefits and Limitations of Hidden Fields
The benefits of using hidden fields to store client-side state include:
- No server resources are required. The hidden field is stored and read directly from the page.
- Almost all browsers and client devices support forms with hidden fields.
- Hidden fields are simple to implement.
- Hidden fields are good for caching data in Web farm configurations because the data is cached on the client.
The limitations of using hidden fields are:
- The hidden field can be tampered with. The information in this field can be seen if the page output source is viewed directly, creating a potential security issue.
- The hidden field does not support rich structures; it offers only a single value field in which to place information. To store multiple values, you must implement delimited strings and write the code required to parse those strings.
- Page performance decreases when you store large values because hidden fields are stored in the page.
Hidden fields are a good solution for caching small amounts of noncritical, string data for use in Web pages.
Implementing Hidden Fields
The following example shows how to declare a hidden field on a Web page.
<input id="HiddenValue" type="hidden" value="Initial Value" runat="server" NAME="HiddenValue">
This code creates a hidden field containing the string data Initial Value.
If you cannot use hidden fields because of the potential security risk, consider using view state instead.
Using View State
Web Forms pages and controls all have a ViewState property, which is a built-in structure for automatically retaining values among multiple requests for the same page. The view state is internally maintained as a hidden field on the page but is hashed, providing greater security than developer-implemented hidden fields do. For more information about view state, see "Introduction to Web Forms State Management," in the MSDN Library.
You can use view state to store your own page-specific values across roundtrips when the page posts back to itself. For example, if your application is maintaining user-specific information, such as a user name that is displayed in a welcome message, you can store the information in the ViewState property.
Performance of view state varies depending on the type of server control to which it is applied. Label, TextBox, CheckBox, RadioButton, and HyperLink are server controls that perform well with ViewState. DropDownList, ListBox, DataGrid, and DataList suffer from poor performance because of their size and the large amounts of data making roundtrips to the server.
In some situations, using view state is not recommended because it can degrade performance. For example:
- Avoid using view state in pages that do not post back to the server, such as logon pages.
- Avoid using view state for large data items, such as DataSets, because doing so increases time in roundtrips to the server.
- Avoid using view state when session timeout is required because you cannot time out data stored in the ViewState property.
View state is a simple and easy-to-use implementation of hidden fields that provides some security features. Most of the performance considerations, benefits, and limitations of view state and hidden fields are similar.
For more information about ViewState performance, see "Maintaining State in a Control" in the MSDN Library.
Benefits and Limitations of View State
The benefits of using view state include:
- No server resources are required because state is contained in a structure in the page code.
- It is simple to implement.
- Pages and control state are automatically retained.
- The values in view state are hashed, compressed, and encoded, thus representing a higher state of security than hidden fields.
- View state is good for caching data in Web farm configurations because the data is cached on the client.
Limitations to using view state include:
- Page loading and posting performance decreases when large values are stored because view state is stored in the page.
- Although view state stores data in a hashed format, it can still be tampered with because it is stored in a hidden field on the page. The information in the hidden field can also be seen if the page output source is viewed directly, creating a potential security risk.
For more information about using view state, see "Saving Web Forms Page Values Using View State," in the MSDN Library.
Implementing View State
The following example shows how to use the ViewState property to store and access user-specific information between page postbacks.
public class ViewStateSample : System.Web.UI.Page
{
private void Page_Load(object sender, System.EventArgs e)
{
if (!Page.IsPostBack)
{
// Save some data in the ViewState property.
this.ViewState["EnterTime"] = DateTime.Now.ToString();
this.ViewState["UserName"] = "John Smith";
this.ViewState["Country"] = "USA";
}
}
#region Web Form Designer generated code
�
#endregion
private void btnRefresh_Click(object sender, System.EventArgs e)
{
// Get the saved data in the view state and display it.
this.lblTime.Text = this.ViewState["EnterTime"].ToString();
this.lblUserName.Text = this.ViewState["UserName"].ToString();
this.lblCountry.Text = this.ViewState["Country"].ToString();
}
}
When your application runs on a low bandwidth client—for example, a mobile device—you can store the view state on the server by using Losformatter and thereby returning slimmer pages with less of a security risk.
Using Hidden Frames
You can use hidden frames to cache data on the client, avoiding the roundtrips to the server that are inherent in hidden field and view state implementations. You create a hidden frame in your page that loads a Web page into the frame containing your data. The pages in your application can access this data using client-side scripting. This implementation does not require any server resources because the data fields in the hidden frame are stored and read directly from the page.
Hidden frames let you secretly load images for use on other pages within the site. When those images are required, they can be obtained from the client cache rather than from the server.
Benefits and Limitations of Hidden Frames
The benefits of using hidden frames to store client-side state include:
- The ability to cache more than one data field.
- The avoidance of roundtrips of data during postbacks.
- The ability to cache and access data items stored in different hidden forms.
- The ability to access JScript® variable values stored in different frames if they come from the same site.
The limitations of using hidden frames are:
- Functionality of hidden frames is not supported by all browsers. Do not rely on hidden frames to provide data for the essential functionality of your pages.
- The hidden frame can be tampered with, and the information in the page can be seen if the page output source is viewed directly, creating a potential security threat.
- There is no limit to the number of frames (embedded or not) that you can hide. In frames that bring data from the database and that contain several Java applets or images, a large number of frames can negatively affect performance of the first page load.
Hidden frames are useful for storing many items of data and resolve some of the performance issues inherent in hidden field implementations.
Implementing Hidden Frames
The following example shows how to declare a hidden frame in a Web page.
<FRAMESET cols="100%,*">
<FRAMESET rows="100%,*">
<FRAME src="contents_of_frame1.html">
</FRAMESET>
<FRAME src="contents_of_hidden_frame.html">
<FRAME src="contents_of_hidden_frame.html" frameborder="0" noresize
scrolling="yes">
<NOFRAMES>
<P>This frameset document contains:
<UL>
<LI>
<A href="contents_of_frame1.html" TARGET="_top">
Some neat contents</A>
<LI>
<A href="contents_of_hidden_frame.html" TARGET="_top">
Some other neat contents</A>
</UL>
</NOFRAMES>
</FRAMESET>
You can then use the data stored in this frame in your Web page by using client-side scripting.
Using Cookies
You can use cookies to store small amounts of infrequently changing information on the client. That information can then be included in a request to the server.
Benefits and Limitations of Cookies
Cookies are useful because:
- No server resources are required. The cookie is stored on the client, sent to the server in a request, and then read by the server after the post.
- They are simple to use. The cookie is a lightweight, text-based structure containing simple key/value pairs.
- They support configurable expiration. The cookie can expire when the browser session ends, or it can exist indefinitely on the client computer, subject to the expiration rules on the client.
The use of cookies may be limited by:
- Size restrictions. Most browsers place a 4096-byte limit on the size of a cookie, although support for 8192-byte cookies is becoming more common in the new browser and client-device versions available today.
- User-configured refusal. Some users disable their browser or client device's ability to receive cookies, thereby limiting the use of cookies.
- Security breaches. Cookies can be subject to tampering. Users can manipulate cookies on their computer, which can potentially represent a security compromise or cause a cookie-dependent application to fail.
- Possible expirations. The durability of the cookie on a client computer is subject to cookie expiration processes on the client and to user intervention.
Note Cookies are often used for personalization, in which content is customized for a known user. In most cases, identification is the issue rather than authentication, so it is usually enough to merely store the user name, account name, or a unique user ID (such as a GUID) in a cookie and use it to access the user personalization infrastructure of a site.
For more information about creating and reading cookies, see "HttpResponse.Cookies Property" and "HttpRequest.Cookies Property," in the MSDN Library.
Implementing Cookies
The following example shows how to store and retrieve information that cookies hold.
public class CookiesSample : System.Web.UI.Page
{
private void Page_Load(object sender, System.EventArgs e)
{
if (this.Request.Cookies["preferences1"] == null)
{
HttpCookie cookie = new HttpCookie("preferences1");
cookie.Values.Add("ForeColor","black");
cookie.Values.Add("BackColor","beige");
cookie.Values.Add("FontSize","8pt");
cookie.Values.Add("FontName","Verdana");
this.Response.AppendCookie(cookie);
}
}
private string getStyle(string key)
{
string val = null;
HttpCookie cookie= this.Request.Cookies["preferences1"];
if (cookie != null)
{
val = cookie.Values[key];
}
return val;
}
}
Never rely on cookies for essential functionality in your Web applications, because users can disable them in their browsers.
Using Query Strings
A query string is information sent to the server appended to the end of a page's URL. You can use a query string to submit data back to your page or to another page through the URL.
Query strings provide a simple but limited way of maintaining some types of state information. For example, they make it easy to pass information from one page to another, such as passing a product number to another page, where it is processed.
Note Query strings are a viable option only when a page is requested through its URL using HTTP GET. You cannot read a query string from a page that has been submitted to the server using HTTP POST.
Benefits and Limitations of Query Strings
Query strings offer the following benefits:
- No server resources are required. The query string is contained in the HTTP request for a specific URL.
- They provide broad support. Almost all browsers and client devices support passing values in a query string.
- They are simple to implement. ASP.NET provides full support for the query string method, including methods for reading query strings using the property.
Query strings do have some limitations; for example:
- The information in the query string is directly visible to the user in the browser user interface. The query values are exposed to the Internet in the URL, so in some cases, security might be an issue.
- Most browsers and client devices impose a 255-character limit on URL length.
Query strings can be very useful in certain circumstances, such as when you are passing parameters to the server to customize the formatting of the data returned.
Implementing Query Strings
The following example shows how to include a query string in a URL.
https://www.cache.com/login.asp?user=ronen
The following example shows how to access the information at the server.
// Check for a query string in a request.
string user = Request.QueryString["User"];
if( user != null )
{
// Do something with the user name.
}
Query strings enable you to send string information directly from the client to the server for server-side processing.
Using Client-Side Caching Technologies
Table 2.4 shows recommendations for the use of each client-side caching technology discussed.
Table 2.4: Client-side state technologies
Mechanism | Recommended uses |
---|---|
Hidden fields | To store small amounts of information for a page that posts back to itself or to another page when security is not an issue. You can use a hidden field only on pages that are submitted to the server. |
View state | To store small amounts of information for a page that posts back to itself. Using view state provides basic security. |
Hidden frames | To cache data items on the client and to avoid the roundtrips of the data to the server. |
Cookies | To store small amounts of information on the client when security is not an issue. |
Query strings | To transfer small amounts of information from one page to another when security is not an issue. You can use query strings only if you are requesting the same page or another page using a link. |
Using Internet Explorer Caching
Microsoft Internet Explorer provides mechanisms for caching pages or page objects on the user's computer. In Internet Explorer, you can cache data on the client rather than on the server, thus reducing server and network load to a minimum.
Internet Explorer caching is not suitable for all situations. In this section, you learn how to best utilize Internet Explorer caching in your applications.
Note Internet Explorer caching is supported only in an Internet Explorer client environment. If other client browsers are also being used, you must use customization code in your application and provide an alternative caching solution for other types of browser.
Understanding Internet Explorer Cache Types
You can store information in the Internet Explorer cache either by specifying a time that the page should expire from the cache or by using dynamic HTML (DHTML) behaviors:
- You can add an EXPIRES directive to the header of objects so that Internet Explorer caches them for the specified time period. When the browser needs to access this data, the expiration date is queried, and if the date is in the future, the cached version of the data is used. If the expiration date has passed, the browser contacts the server for the current version of the data. As a result, any sites that use the EXPIRES header perform better.
- You can use persistence through DHTML behaviors that allow you to store information on the client. Doing so reduces the need to query a server database for client-specific information, such as color settings or screen layout preferences, and increases the overall performance of the page. Persistence stores the data hierarchically, making it easier for the Web developer to access.
Note Users can choose to work offline by selecting Work Offline on the File menu in Internet Explorer. When Work Offline is selected, the system enters a global offline state independent of any current network connection, and content is read exclusively from the cache.
Another method of making Internet Explorer cache pages is by manually setting the page expiration by using the Web pagefile properties in IIS. The site administrator can perform this operation.
To set the expiration date manually in IIS
- Right-click the file name.
- Click Properties, and then click the HTTP Headers tab.
- Select the Enable Content Expiration check box.
- Set a date in the future, and then click OK.
For more information about the Internet Explorer cache, see "Control Your Cache, Speed up Your Site" in the MSDN Library.
Determining What to Cache in the Internet Explorer Cache
You should use the Internet Explorer cache for static page items that do not change often, such as:
- Images and bitmaps on HTML pages.
- Static text objects.
- Page banners and footers, which contain general information or navigation menus that seldom change. Retrieving this data from the Internet Explorer cache gives the user an immediate response while the rest of the page data is fetched from the server.
- Site home pages that rarely change. This page can be stored in the Internet Explorer cache to give the user a prompt response when navigating to the site.
- Client-specific data that uses DHTML persistence. This solution is limited to browsers that support DHTML, such as Internet Explorer 4.0 and later.
Do not use the Internet Explorer cache for dynamic content because the cached copy on the client soon becomes stale.
Understanding Benefits and Limitations of the Internet Explorer Cache
Using the Internet Explorer cache provides the following benefits:
- Reduced network traffic because the client does not need to contact the server for all of the content.
- Facilitation of offline browsing. Internet Explorer cached data is available for offline viewing on the client when it is disconnected from the network.
- The ability to build hierarchical data structures using the XML Document Object Model (DOM), using only DHTML persistence. The page author can use the formatted data structure supported by XML standards to store complex data in the cache, which is easy to navigate and retrieve.
The Internet Explorer cache does have some limitations:
- Cached data expiration time has to be predetermined and cannot depend on server data updates. Internet Explorer currently supports a lazy update scheme, in which cached data loads first. Then Internet Explorer queries the server to check whether the data has been updated. If so, the data is fetched to the Internet Explorer cache to prepare for the next time the client requests the data.
- Internet Explorer cached data is not encrypted. You must avoid storing sensitive data such as credit card numbers in the Internet Explorer cache because anyone with access to the cache folder can view the files and the data cached in them.
The Internet Explorer cache is ideal for improving performance for pages containing large static items.
Implementing Internet Explorer Caching
Add the following header to your page or object header to control how Internet Explorer caches the page.
<META HTTP-EQUIV="expires" CONTENT="Tue, 23 Jun 2002 01:46:05 GMT">
This example caches the page or object until the date specified by the value attribute.
Managing the Internet Explorer Cache
Internet Explorer lets the user manage the cache size and cache update frequency by changing the settings in the Internet Explorer Options dialog box. You can change the cache size to match the amount of free disk space on the client computer.
The Internet Explorer cache can be managed only from the client computer and cannot be controlled from the server.
Summary
This chapter introduced you to various technologies available for implementing caching mechanisms in .NET-based applications. You saw examples of how to implement these mechanisms and learned the benefits and limitations of each.
The next step is to map how to use these technologies in the various application layers and deployment scenarios in distributed applications.