Caching Architecture Guide for .NET Framework Applications
Caching .NET Framework Elements
Summary: This chapter covers how you can ensure your own classes are cacheable, and discusses caching techniques that can be used with common .NET Framework objects.
Preceding chapters explained caching, including available caching technologies and the appropriate locations for the different types of cached data. In this chapter, you learn about how to make your own classes cacheable, what issues to consider when you implement caching, and what types of Microsoft .NET Framework elements can be cached in a distributed application.
This chapter contains the following sections:
- "Planning .NET Framework Element Caching"
- "Implementing .NET Framework Element Caching"
Planning .NET Framework Element Caching
You must consider various coding implementation issues when planning to cache .NET Framework elements in your applications. Such issues include:
- Ensuring thread safety
- Cloning
- Serializing
- Normalizing cached data
- Choosing a caching technology
Some of these issues can result in inconsistent data, whereas some simply influence the application's efficiency.
Ensuring Thread Safety
When you write multithreaded applications, you must ensure that threads interact properly. For example, when a program or routine can be called from multiple threads, you must ensure that the activities of one thread do not corrupt information required by another. A routine is considered thread safe when it can be called from multiple programming threads without unwanted interaction among them.
When you are caching data types that are type safe, you can eliminate the risk that one thread will interfere and modify data elements of another thread through coordinated access to shared data, thus circumventing potential data race situations.
If your code is not thread safe, the following can occur:
- A cached item is read from the cache on one thread and then updated on another, resulting in the use of inconsistent data.
- A cached item is flushed from the cache by one thread while a client using another thread tries to read the item from cache.
- Two or more threads concurrently update a cached item with different values.
- Remove requests result in an item being removed from the cache while it is being read by another thread.
You can avoid these problems by ensuring that your code is thread safe, allowing only synchronized access to the cached data. You can do so either by implementing locking in the caching mechanism or by using the ReaderWriterLock class in the cache items.
Implementing Thread Safety Using the Cache
You can implement thread safety by using the following methods when using a cache:
- Action-level locking—You can implement thread safety in the Insert, Update, Get, and Delete cache interfaces. In this scenario, the cache automatically locks an item when it is added to the cache or updated.
- Cache-level locking—You can provide Lock and Unlock methods that the client code uses when thread safety is needed. The client needs to call the Lock method before updating the cached item and call Unlock afterward.
To find out whether a specific cache technology supports synchronization and thread safety, see "Choosing a Caching Technology," later in this chapter.
Implementing Thread Safety in the Cached Items
If your chosen caching mechanism does not provide thread safety, you can implement thread safety directly in your cached objects by using the ReaderWriterLock class from the System.Threading namespace in the .NET Framework. For more information about this class, see "ReaderWriterLock Class," in the MSDN Library.
The following example shows how you can use the ReaderWriterLock class to implement thread safety in cached items.
class Resource {
ReaderWriterLock rwl = new ReaderWriterLock();
Object m_data; // Your class data
public void SetData(Object data) {
rwl.AcquireWriterLock(Timeout.Infinite);
// Update your object data.
rwl.ReleaseWriterLock();
}
public Object GetData() {
// Thread locks if other thread has writer lock
rwl.AcquireReaderLock(Timeout.Infinite);
// Get your object data...
rwl.ReleaseReaderLock();
return m_data;
}
}
Ensuring thread safety of cached items guarantees synchronized access of the items and avoids such issues as dirty reads and multiple writes.
For more information about thread synchronization, see "Safe Thread Synchronization" in the MSDN Magazine.
Cloning
Another solution to thread safety and synchronization issues is to use cloning. Instead of obtaining a reference to the original copy of your object when retrieving cached items, your cache can clone the object and return a reference to the copy.
To do so, your cached objects must implement the ICloneable interface. This interface contains one member, Clone(), which returns a copy of the current object. Take care to implement a deep copy of your object, copying all class members and all objects recursively referenced by your object.
If you design your cache to include interfaces for receiving both a cloned object and a reference to the original cached object, you will find new possibilities for resolving synchronization issues in your application:
- If your object is not thread safe, you can use cloning to create a copy of your cached data, update it, and then update the cache with the new object. This technique solves synchronization issues when data is updated. In this scenario, updating the cached item is performed as an atomic action, locking the access to the object during the update.
- One process can be responsible for updating an object while another process uses a cloned copy of the object for read-only purposes. This solves the problem of synchronizing read-after-write operations on your cache because the read object is merely a copy of the cached object, and the original object can be updated at the same time.
- Write-after-write operations on the cached object should still be synchronized, either by the cache or by the object, as described in the "Ensuring Thread Safety" section earlier in this chapter.
Figure 4.1 shows how cloning can solve synchronization problems when there is one writer but many users.
Figure 4.1. Cloning cached objects
Figure 4.1 shows the following steps in a sample cloning process:
- Process A gets a clone of a cached object and updates the cloned object.
- Process B gets a read-only clone of the cached object.
- Process A updates the object in cache from its cloned copy without affecting the copy process that B is using.
The next time process B requests the object, it receives the new version of the object that process A updated.
Note An alternative solution to synchronization issues is for process A to create a new object instead of cloning an existing cached object and then to update the cache with the new object.
Serializing a .NET Class
When sharing a cache between several processes or across an application farm, the cached items must be serialized when being inserted into the cache and deserialized when retrieved. An example of this is when you are storing ASP.NET session state in a SQL Server database. For more details, see the "Using ASP.NET Session State," section in Chapter 2, "Understanding Caching Technologies."
Because memory cannot be shared between two processes or between two computers, your cached object must be converted into a format such as XML or a binary representation to transport it between the memory locations. Consider this fact when designing your .NET classes, and if you plan to use a caching technology that requires objects to be serialized, you must make your .NET class serializable. You can do so by using the Serializable attribute or by custom serializing your classes by implementing the ISerializable interface.
Note To see whether a specific caching technology requires serialization support, see "Choosing a Caching Technology," later in this chapter.
The following example shows how to use the Serializable attribute to make a class serializable.
[Serializable]
public class MyClass {
�
}
For more information about serializing a .NET class by using the Serializable attribute, see "Basic Serialization" in the MSDN Library.
The following example shows how to custom serialize a .NET class by implementing the ISerializable interface.
public class MyClass1: ISerializable
{
public int size;
public String shape;
//Deserialization constructor
public MyClass1 (SerializationInfo info, StreamingContext context) {
size = (int)info.GetValue("size", typeof(int));
shape = (String)info.GetValue("shape", typeof(string));
}
//Serialization function
public void GetObjectData(SerializationInfo info, StreamingContext
context)
{
info.AddValue("size", size);
info.AddValue("shape", shape);
}
}
For more information about custom serialization, see "Custom Serialization" in the MSDN Library.
It is recommended that you implement ISerializable instead of using the Serializable attribute because ISerializable does not use reflection and results in better performance.
For more information about serializing a .NET class, see "Run-time Serialization, Part 2" and "Run-time Serialization, Part 3" in the MSDN Magazine.
Normalizing Cached Data
When you are caching data, store it in the format in which clients use it; this is known as normalizing cached data. Normalizing cached data is a process in which an object is constructed once and then accessed as needed rather than being constructed anew each time it is accessed. For example, if you are caching a polygon for display in your application's user interface, and the polygon details are saved in a database as a set of points, do not cache these points in the user interface. Instead, construct the polygon class, and cache that object.
By definition, normalizing cached data involves transforming your cached data to the format that is needed for later use. When you normalize your cached data, you ensure that:
- You can easily retrieve and use data from the cache; no formatting or deserialization is required.
- Repetitive transformation of the data each time it is retrieved from the cache is avoided.
Note This definition of normalization differs from the classic data processing definition of the term, which is concerned with saving the data in the most efficient and memory-saving form. When you consider normalizing cached data, your first priority is usually performance rather than memory.
Choosing a Caching Technology
To choose an appropriate caching technology for your application, consider which caching technologies require serialization support in cache items and which provide synchronization mechanisms. Table 4.1 compares the available caching technologies and their support for serialization and synchronization.
Table 4.1: Caching technologies
Technology | Requires serialization support in items | Provides synchronization mechanism |
---|---|---|
ASP.NET | ||
Programmatic data cache | No | Yes |
Page output cache | Not applicable | Not applicable |
Page fragment cache | Not applicable | Not applicable |
Session state—InProc | No | No |
Session state—StateServer | Yes | No |
Session state—SQLServer | Yes | No |
View state | Yes | Not applicable |
Hidden fields | Yes | Not applicable |
Cookies | Yes | Not applicable |
Query strings | Not applicable | Not applicable |
Internet Explorer cache | Not applicable | Not applicable |
Remoting singleton | Yes | Custom implementation required |
Memory mapped files | No | No |
SQL Server | Yes | Yes |
Static variable | No | Custom implementation required |
After you consider the issues involved when caching .NET Framework elements in your applications, you can begin to think about what elements to cache and where to cache them.
Implementing .NET Framework Element Caching
Some of the .NET Framework elements are excellent candidates for caching. In this section, you will learn about some of these elements and where they should be cached.
Caching Connection Strings
Database connection strings are difficult to cache because of the security issues involved in storing credentials that are used to access your database—for example, in Machine.config or Web.config—because the .NET Framework automatically caches the configuration files. However, because the configuration files are stored as clear ASCII text files, encrypt your connection strings before entering them in the configuration files and then decrypt them in your application before using them.
For more information about securely caching connection strings in your applications, see the guidelines for using Data Protection API (DPAPI) in the "Storing Database Connection Strings Securely" section of "Building Secure ASP.NET Applications" in the MSDN Library.
For more information about using DPAPI encryptions, see the following articles in "Building Secure ASP.NET Applications," in the MSDN Library:
- "How To: Create a DPAPI Library"
- "How To: Use DPAPI (Machine Store) from ASP.NET"
- "How To: Use DPAPI (User Store) from ASP.NET with Enterprise Services"
These How To articles contain step-by-step instructions to help you learn how to use DPAPI encryption in your ASP.NET applications.
Caching Data Elements
Because of the relatively high performance costs of opening a connection to a database and querying the data stored in it, data elements are excellent caching candidates and are commonly cached. In this section, you learn whether or not to cache DataSet and DataReader objects.
Caching DataSet Objects
DataSet objects are excellent caching candidates because:
- They are serializable and as such can be stored in caches either in the same process or in another process.
- They can be updated without needing to reinsert them into the cache. Because you cache a reference to your DataSet, your application can update the data without the reference in the cache needing to change.
- They store formatted data that is easily read and parsed by the client.
Caching frequently used DataSet objects in your applications often results in improved performance.
Caching DataReader Objects
Never cache DataReader objects. Because a DataReader object holds an open connection to the database, caching the object extends the lifetime of the connection, affecting other users of the database. Also, because the DataReader is a forward-only stream of data, after a client has read the information, the information cannot be accessed again. Caching it would be futile.
Caching DataReader objects disastrously affects the scalability of your applications. You may hold connections open and eventually cache all available connections, making the database unusable until the connections are closed. Never cache DataReader objects no matter what caching technology you are using.
Also do not cache SqlDataAdapter objects because they use DataReader objects in their underlying data access method.
Caching XML Schemas
The .NET Framework includes the XmlSchemaCollection class that can help cache XML-Data Reduced (XDR) and XML Schema Definition language (XSD) schemas in memory instead of accessing them from a file or a URL. This makes the class a good caching candidate.
The XmlSchemaCollection class has methods for adding schemas to the collection, checking for the existence of a schema in the collection, and retrieving a schema from the collection. You can also use the XmlSchemaCollection to validate an XML document against XSD schema.
The XmlSchemaCollection improves performance by storing schemas in the cached collection so that they do not need to be loaded into memory each time validation occurs. For more information about the XmlSchemaCollection, see "XmlSchemaCollection as a Schema Cache," in the MSDN Library.
Caching Windows Forms Controls
Caching Windows Forms controls in your user interface can improve responsiveness of the user interface. Because Windows Forms controls that include structured data require you to populate them with the necessary data before displaying them to the user, applications may appear sluggish. However, because it is the data loading that takes a long time, caching the populated version of these controls improves performance.
A static variable cache is the natural choice for caching Windows Forms controls because it has the following attributes:
- Application scope—Your Windows Forms controls are used within the scope of your Windows-based application, which matches the scope of your cache.
- Simple implementation—A static variable cache is an ideal choice for a simple caching implementation if you do not need advanced features such as dependencies, expirations, or scavenging.
- Good performance—Because the static variable cache is held in the application's memory space, all caching calls are within the same process space, resulting in good performance.
You can use Windows Forms controls in only one page at a time. Because the cache holds a reference to the control, you have only one copy of the control to use at any given time. It does not make sense to clone Windows Forms controls because most of the work is in populating your control, and cloning effectively creates a new control. An exception to this is if you need to concurrently use the same class in several forms in your application. In this situation, you can clone several instances of the control to be used concurrently by all forms requiring the control.
The following Windows Forms controls are classic examples of good caching candidates:
- DataGrid—Displays ADO.NET data in a scrollable grid
- TreeView—Contains a Nodes collection of TreeNode objects that implement ICloneable
- ListView—Contains an Items collection of ListViewItem objects that implement ICloneable
Because these controls often require complex processing before being rendered, caching them can improve application performance. Cache controls that take a long time to populate with data or that have complex initialization (that is, constructor) code. You may also decide to cache controls that are used in multiple forms in your application user interface.
The following example shows how to create a cache based on a hash table.
static Hashtable m_CachedControls = new Hashtable();
The following example shows how to create a TreeView control and insert it into the cache that has been created.
private void btnCreate_Click(object sender, System.EventArgs e)
{
// Create the TreeView control and cache it.
TreeView m_treeView = new TreeView();
m_treeView.Name = "tvCached";
m_treeView.Dock = System.Windows.Forms.DockStyle.Left;
m_CachedControls.Add(m_treeView.Name, m_treeView);
}
The following example shows how to obtain the control from cache and bind it to a form
private void GetAndBindControl(Form frm)
{
frm.Controls.Add((Control)m_CachedControls["tvCached"]);
frm.Show();
}
These code samples are all you need to implement Windows Forms control caching in your application.
Caching Images
Because images are relatively large data items, they are good caching candidates. An image can be loaded from various locations, including:
- A local disk drive
- A network drive on a local area network (LAN)
- A remote network location or URL
Loading from local drives is faster than the other options; however, images cached in memory load extremely quickly and improve your application's performance and the user's experience.
Before you decide where to cache an image, consider how the image is used in the application. There are two common scenarios for image usage:
- Images displayed to the user in the presentation tier of an application
- Images used in the business tier, such as an image processing application in which images are used by different image processing algorithms
Following the rule that data should be stored as closely as possible to where it is used, the caching solution for each of these scenarios is unique.
Caching Presentation Tier Images
Two types of presentation tier caching exist: Web client caching and rich client caching. They use similar caching principles are similar, but you should choose different caching technologies.
Caching Images in a Web Client
When caching images in a Web client, do the following:
Use the ASP.NET cache as your caching technology.
Explicitly manage the caching of images on the page. Images contained on a page are loaded using separate HTTP requests, and they are not automatically cached when using ASP.NET page caching.
Use file dependencies on the cached image file to ensure that the image is invalidated when the data changes.
Consider your cache expiration time, taking into account:
- Whether your images are static (never change) or dynamic
- Whether your images change at known intervals
- Whether your image refresh time is dependent on other cache keys or on disk files
For more information about specifying cache expiration times for images in Web applications, see "Using Dependencies and Expirations" in Chapter 2, "Understanding Caching Technologies."
Caching Images in a Rich Client
When caching images in a rich client, do the following:
Use the static variable cache as your caching technology. Static variables give you both good performance and simple development.
Cache only if the source of the images is outside your application assembly. It is common practice to store images as statically linked resources in the application assembly, in which case the images are loaded into memory with the assembly, and caching is not necessary.
Cache images that are being retrieved from a database store or from a remote computer on the local disk when the images are first read, and then read them from the disk to service future requests.
Figure 4.2 shows how you can cache remotely stored images on a local disk for future use.
Figure 4.2. Caching image files from a database
Caching images in the presentation tier helps increase the user-perceived performance of your application.
Caching Business Tier Images
When caching images in the business tier, do the following:
- If all clients that are using the cached image are in the same process or AppDomain, use either the ASP.NET cache object or a static variable cache as your caching mechanism. Both of these methods facilitate simple programming models, but both have limitations:
- Static variables do not offer any cache-specific features such as dependencies, expiration, and scavenging.
- ASP.NET caching requires you to install ASP.NET on your servers in the business tier.
- If your cached image needs to be accessed from different processes, implementing a memory-mapped file cache. Memory-mapped file caching offers very good performance at the cost of complicated implementation.
Caching images in either the presentation tier or the business tier of your applications can enhance the performance because it reduces the time needed to acquire and render image.
Caching Configuration Files
The .NET Framework automatically caches.NET configuration files (for example, Machine.config and Web.config) by using the internal .NET caching mechanisms. Because of this, you can use the .NET configuration files without implementing any caching mechanism yourself. Although the configuration files are cached by the .NET Framework, you should remember the following facts when working with them in an application that caches data:
- Changes to the Global.asax and Web.config files restart the AppDomain, which resets your ASP.NET cache, application state, and session state (if using InProc mode).
- Changes to the Machine.config file restarts the ASP.NET worker process (Aspnet_wp.exe).
Therefore, you should not scan configuration files for viruses. Doing so restarts the application each time.
Caching Security Credentials
The .NET Framework includes a class that you can use to cache multiple security credentials. This is useful when you are accessing multiple network resources that require different credentials, such as Web sites or Web services from different vendors. For example, in the .NET domain, your application may be required to authenticate with different service providers, whether they publish Web services that your application uses or have Web sites accessed by your application that require you to authenticate upon entering.
The NetworkCredential class represents credentials for password-based authentication schemes, such as basic, digest, NTLM, and Kerberos authentication. Use the NetworkCredential class to store your authentication data when caching security credentials because the authentication data is stored formatted, ready to use in your application.
Summary
This chapter helps you plan the caching of .NET Framework elements and covers the options available for doing so.
After you decide what to cache, where to cache it, and how to cache it, you must consider how to get the data into the cache and how to manage it once it's there.