Abstract Cache


Intent

Provide a simple and abstracted means of caching expensive objects. Abstract the container used for holding any of the cached objects (this isolates the architecture from any specific cache container implementation). Place business rules for determining caching semantics (e.g., timeouts) in the objects being cached where they belong. Provide a standard means by which to build a cache and execute cacheable services.

Problem

Improving performance is usually never considered a bad thing. However, those attempts to improve the performance often end up compromising the design. These attempts include tasks such as denormalizing a database, adding complex thread logic, or using platform-specific caching techniques such as those used in ASP applications. Besides compromising the design, sometimes these "improvements" can have the ill effect of actually slowing down performance. When dealing with the overhead of thread object creation or holding state for caching, the application can sometimes take on the opposite effect from what was intended. Designers should guard themselves from technology- or platform-specific approaches. For threading, this may be more difficult. For caching, however, this can be achieved more readily.

Implementing caching should not slow down a system, nor should the design. Most caching implementations are centralized with some form of "controller." The controller usually takes whatever stateful caching container may be accessible and is in charge of placing content into and out of the cache. Business data must somehow be fed into the cache through the controller, forcing the design to take this into account and thus increasing unnecessary cohesion between the cache controller and the business object that provides the data for the cache. Staying with this scenario, business rules must now be maintained in the cache controller so that the caching semantics can be enforced. These semantics include timeout periods, "stale" data indicators, size restrictions, etc. The cached controller soon becomes an expert in business caching semantics that it should never be aware of.

Most caching implementations utilize a technology-specific means of caching. For ASP applications, the application or session object may be used to cache simple values (although it is not recommendedat least not until .NET arrived). In the MTS and COM+ world, the shared Property Manager (SPM, or "spam") can be utilized to hold cached or stateful data (again, not a recommended practicethe spam has its own issues). Even if the technology is solid, tying an entire framework to one caching technology could become a problem. For .NET applications that wish to be platform-independent someday, using anything technology-specific may not be a good idea.

Caching, at least regarding the performance of critical applications, should become part of the framework at some point. The cached objects themselves should decide how, when, and whether to place objects in the cache. Caching should not itself slow down the system with unnecessary overhead. The container used for holding cached objects should be as technology- agnostic as possible, thus providing a migration path for easily incorporating new containers. Finally, the caching design should be simple enough for all business objects to take advantage of and integrate into the overall architecture.

Forces

Use the Abstract Cache Pattern when:

  • Expensive operations can be pre-prepared.

  • The application requires performance improvements (when doesn't it?).

  • The design cannot tie itself to any one technology-specific caching implementation.

  • There are specific rules for caching content, such as expiration, size, etc.

  • Several business objects are candidates for caching.

Structure

The following structure (Figure 7.1) shows the pieces of our "caching puzzle" generically. The cache adapter is the driver of the cache object. The cache object is the container itself and will be driven by whatever client code performs the initial caching action. The ConcreteCacheable class contains the element about to be cached and provides the method or methods used to house caching logic within the architecture. This typically will be a key signature method through the architecture typically housed in a base class. The implementation section will go into more detail on the mechanics of this. The structure of the Abstract Cache pattern can be seen in Figure 7.1.

Figure 7.1. Abstract Cache class diagramgeneric model.

graphics/07fig01.gif

Consequences

The Abstract Cache pattern has the following benefits and liabilities:

  1. Abstracts the cache container implementation . Using this pattern, any custom cached container can be used. For this example, we use the System.Web.Caching.Cache object for holding all objects and data in the cache. Any custom cache should implement the same interfaces as are provided by .NET's cache object. It should be thread-safe and durable. The only entity that is bound to the cache container type is the client (ReportingWebService, in our example) for construction and the CacheAdapter for aggregating the cache container.

  2. Places caching logic with the objects and/or data being cached . This is probably the best benefit of using this pattern. By passing the cache container in the form of CacheAdapter to a business object that may have data to cache, it allows business rules for caching to be placed where they belongin the business object. The business object is then given the opportunity to cache data. It also is given the opportunity to specify the necessary rules of the particular cached element.

  3. Provides a simple framework upon which to build all cacheable content . Caching can become a complex operation if not monitored . Standardizing the way in which all business objects may cache elements is crucial to ensuring a robust framework that can actually utilize the feature. Providing a standard means of controlling a cache also centralizes the cache (great for debugging) without coupling business logic in its container or its aggregator.

  4. Provides a performance improvement for expensive and repeatable operations . This shouldn't require much explanation and comes under the category of "no kidding." One point to keep in mind, however, is that the last thing you want to do is slow down a system after implementing a caching scheme, so take heed when doing so. Cache only objects and/or data that are very expensive to generate. This includes file I/O, database I/O, or any expensive initialization. Another candidate for the cache is to preinstantiate third-party components. As was mentioned in the Product Manager pattern in Chapter 4, building an unmanaged proxy can become a bottleneck. This usually refers only to custom COM components components such as those that have been written for a specific business purpose that needs to be leveraged and may contain expensive initializations. COM will take care of most of the caching concerns from a runtime level. This is especially true if the components happened to be COM+ components.

    What I am referring to are those custom components that may contain, for example, expensive collections that need to be hydrated, initialized , or whatever. This is where caching from .NET can add benefit. .NET can be leveraged for its advantages because caching was never really addressed as a customizable option (via the Cache object) before .NET or ASP.NET were introduced.

  5. May slow down the system if not applied correctly or if used for single transactions . Not much more to say here that wasn't said in the above point, except that it would be advisable to perform some form of instrumentation to test for bottlenecks. Instrumentation can help determine whether indeed there really is a performance bottleneck, where it is located, and how you may eliminate it. Instrumentation can take the form of simple a System.Diagnostics.Trace.WriteLine("spit out timespan") or as sophisticated as what a third-party "profiler" tool would provide. Such tools as Compuware's DevPartner Profiler (formerly Numega) for .NET can typically provide enough information to determine what areas of code may be of concern. However, there still may be cases where some form of custom instrumentation will be needed. This is especially true when working with a complex framework driven from Web services that may be located on another machine.

Participants

  • Client (ReportingWebService) This refers to the client of the object containing information to be cached. This is not to be confused with strictly a front-end client, although it could be. For our example, the client is a reporting Web service that acts as a controller to business objects containing expensive operations. Here, the ReportingWebService client will retrieve a reference to the Context.Cache that is accessible from the System.Web.Caching namespace from the Web service code. The client is in charge of creating the CacheAdapter, passing in the cache container (Context.Cache), and calling the business objects preparation method, PrepareCache().

  • ConcreteCacheable (ComplexReport) This is any business object that may contain expensive operations. This is where the magic really happens. For our example, the ComplexReport object must implement a factory method, PrepareCache(), by which it will receive a reference to a CacheAdapter. Using the CacheAdapter, it will determine whether it needs to add items to the cache, determine all caching parameters, or return items from the cache. For this example, we have simplified where the entire business object itself gets cached. This can be easily altered so that only specific items get cached, as long as those items implement a standard interface such as ICache.

  • Cache (same) This is a durable, stateful cache container typically implemented by a technology-specific source. For our example, this is the Context.Cache because it is accessed from the ReportingWebService, or any Web service, for that matter. A custom Cache object can be created as long as it implements similar logic to the out-of-the-box version that comes with .NET. The whole point of implementing the CacheAdapter class is to isolate the framework from whatever cache container is used.

  • CacheAdapter (same) This is the standard class used to aggregate a selected cache container. Its sole purpose is to standardize and control what functionality is exposed from a cache container and to isolate the user of the cache from any technology-specific container. ConcreteCacheable objects speak only to the CacheAdapter, and the CacheAdapter delegates all calls to the cache container.

  • ICache (same) This main interface for all ConcreteCacheable objects will implement what will be executed from the cache. This standardizes the caching framework and abstracts the implementation of how items in the cache are implemented. This is the interface used by the client to execute items from the cache.

  • ICacheable (same) This is implemented by all ConcreteCacheable objects. It contains the framework-specific preparation methods used to "build" the cache once the PrepareCache method is called. This also helps standardize the caching framework so that all business objects with cacheable information can take advantage of it with simplicity.

  • IEnumerable (same) This interface is used so that items in the CacheAdapter can be iterated like the most specific cache containers. This is implemented by the CacheAdapter, and its single GetEnumerator call is delegated to its aggregate (the Cache object, in our example).

Implementation

To implement this pattern, you first need to determine which business operations are sufficient candidates for caching. For our example, the ComplexReport object must retrieve configuration data from an XML file on the local system that will be used to format the report. This configuration information, which will not change from execution to execution, is persisted . It thus requires file I/O and is rather large (well, for our demo purposes it isn't). The ComplexReport object is called numerous times a day by each client wishing to view its report and is expensive. This makes it a great candidate for caching. For our example, we use a Web service to drive the report called, appropriately enough, ReportingWebService . Here, the System.Web.Caching.Cache object reference is retrieved, a CacheAdapter is built, and we create the ComplexReport object using the PrepareCache factory method . To see my implementation of the Abstract Cache pattern reference Figure 7.2

Figure 7.2. Abstract Cache class diagramimplementation example.

graphics/07fig02.gif

To determine the key to use for storing the cached object, we use SOAP headers to pass in the key from the client. Using SOAP headers or passing the key from the client is just an option but we found it to be quite useful. Using SOAP headers keeps us from having to pass the key for each method that may drive a cached object. (See the following technology backgrounder for more information on SOAP headers.) If you do not intend to use SOAP headers, will use them strictly from within .NET both from the client and the server, and/or do not wish to have a deeper understanding of them, you can skip the following technology backgrounder. This is not a book on the details of the SOAP protocol. However, a brief understanding of what SOAP headers are, at least from the SOAP Toolkit Version 2.0 viewpoint, will be very helpful for future debugging.

Technology BackgrounderA Look at SOAP Headers

Microsoft's SOAP Toolkit allows any message to contain header content in addition to the normal body content. Web service clients may often wish to pass information between a client and a Web service that may not be part of the Web method being requested . For example, a Web service client may request a Web service that first requires login credentials to be passed to its Web methods. This same login method, once authenticating the user, may return a session ID or token back to the Web service client so that return invocations can be authenticated. Without SOAP headers, the client would be required to pass this token into every Web method located on the server, thus forcing each Web method signature to require one or more parameters. With SOAP headers, repeatable parameters such as session tokens can now be sent as part of the header request, thus keeping each public Web method clean from passing this unnecessarily. Your stack will be happier , and the incoming SOAP message will be clearer. When debugging Web methods, this benefit will become all too clear.

If you will not be using .NET for passing SOAP headers to a Web service, you will have a few more steps to perform. This would be the case if your Web service clients could not be guaranteed to have the .NET framework installed on XP or Windows 2000 or if you were not running Windows 2003. Instead, you must rely strictly on XML. This is where the SOAP Toolkit comes into play. The SOAP Toolkit 2.0 allows headers to be accessed and created on both the client and the server. To package and send headers from the client, you must provide a COM component that implements the IHeaderHandler interface. Once this component is implemented, it must be passed as the HeaderHandler property of the SoapClient component. The SoapClient is the main component used for sending SOAP requests using the toolkit. Pure XML could also be used, as long as it conforms to the SOAP specification, but using the component will save time.

Each time that the client application calls one of the methods of the SoapClient component, it will call the willWriteHeaders method of the header handler. If this method returns true, the SoapClient will write a SOAP <Header> element to the request message, then will call the writeHeaders method of the header handler. This method uses the specified SoapSerializer object to write any header content that is desired, such as the VB sample code in Listing 7.1. This client sample happens to work with most Web services written in C#. This is unlike some of the sample code in the SOAP Toolkit or other non-Microsoft toolkits, for that matter, such as Apache.

Listing 7.1 SOAP Toolkit Visual Basic header serialization example.
 Private m_Token As String Private Const HEADER_ELEMENT_NAMESPACE As String = _ "http://www.etier.com/patterns.net" Private Function IHeaderHandler_readHeader(ByVal pHeaderNode As _ MSXML2.IXMLDOMNode, ByVal pObject As Object) As Boolean If pHeaderNode.baseName <> HEADER_ELEMENT_NAME Or _ pHeaderNode.namespaceURI <> HEADER_ELEMENT_NAMESPACE Then       IHeaderHandler_readHeader = False       Exit Function     End If     m_Token = pHeaderNode.selectSingleNode("SessionId").Text     IHeaderHandler_readHeader = True End Function Private Function IHeaderHandler_willWriteHeaders() As Boolean     IHeaderHandler_willWriteHeaders = True End Function Private Sub IHeaderHandler_writeHeaders(ByVal pSerializer As _       MSSOAPLib.ISoapSerializer, ByVal pObject As Object) Dim sElementText As String Dim sXML As String     pSerializer.startElement "SessionHeader"     pSerializer.SoapAttribute "xmlns", , _       HEADER_ELEMENT_NAMESPACE     pSerializer.startElement "SessionId"     pSerializer.writeString "121938123"     pSerializer.endElement     pSerializer.endElement End Sub 

For more on SOAP serializers, please refer to the SOAP Toolkit 2.0 documentation. Finally, the SoapClient writes the body of the request message, sends it to the server, and eventually receives a response. The SOAP message can be captured (traced) in several ways. You can use the MsSoapT.exe trace utility that ships with the SOAP Toolkit 2.0 or you can use a TraceExtension attribute in .NET from the Web service itself. To see all of a service's messages received from all SOAP clients using MsSoapT.exe, perform the following steps on the server.

  1. Open the Web Services Description Language (WSDL) file.

  2. In the WSDL file, edit the <soap:address> element as part of the service element and change the location attribute to port 8080. (For .NET Web services, you must first save the http://foobarwebservice?wsdl output as a separate file and invoke the edited version directly.) For example, if the location attribute specifies <http://localhost/pattens.net/webservicefactory.wsdl>, change this attribute to <http://localhost:8080/pattens.net/webservicefactory.wsdl>.

  3. Run MsSoapT.exe.

  4. Select New from the File Menu and select Formatted Tracing.

  5. Accept the default values.

The XML snippet in Listing 7.2 shows what a SOAP header request would look like as sent from a SOAP 2.0 client.

Listing 7.2 SOAP message header example.
 <?xml version="1.0" encoding="UTF-8" standalone="no" ?> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header> <SessionHeader xmlns="http://www.etier.com/patterns.net  ">  <SessionId>  121938123  </SessionId>    </SessionHeader>  </SOAP-ENV:Header> <SOAP-ENV:Body> ...  </SOAP-ENV:Body> </SOAP-ENV:Envelope> 

To use headers on a server not using .NET, you must provide a COM component that also implements the IHeaderHandler interface and identify that class in the appropriate <service> element of a suitable Web services meta-language (WSML) file. Unfortunately, I won't be covering the handling of SOAP headers from the server because most Web service development should take place from .NET and is the obvious focus of this book. To access the previous request SessionHeader from .NET and C#, keep reading.

In the following code, the service client creates a Web service object ( assuming that a Web reference was added) by first instantiating the Etier3.LazyCacheService class. Using this object, it can then set the SoapHeader variable called sSessionId . The session ID is a data member of the Etier3.SessionHeader class, which is a nested type of the Etier3.LazyCacheService class. Etier3 just happens to be the name of the machine this Web service is deployed on and is the name of the Web service reference. To set the session ID of the SessionHeader class, you directly create it like any other type and set its data members . The final step in this Web service and SOAP header client is simply to call an operation on oService. The SOAP headers will be passed automatically. Using C# or any other .NET language makes it simple. Using a non-.NET client such as VB6 is possible but more complicated, especially when working with SOAP headers. As mentioned in the previous technology backgrounder, any client that can format and send an XML request can be used. But be forewarned, there is much more work to be done the lower you go in the level of implementation. I suggest using .NET whenever possible, a positive proposition to be sure.

Listing 7.3 Using .NET to set SOAP headers.
 Etier3.ReportingService oService = new Etier3.ReportingService(); // bind session id to soap header oService.SessionHeaderValue = new Etier3.SessionHeader(); oService.SessionHeaderValue.sSessionId = txtSessionId.Text; // no need to pass session as part of the web service signature, // it will be included in the soap header // first call should cache any cacheable objects oService.Execute(true); // for demo purposes only -- second call will use cache oService.Execute(true); 

From this point, the Web service receives the request along with the any set members of the SOAP headers. Here the SOAP Header class is defined and declared using oSessionHeader, along with the SoapHeaderAttribute declaration. Here we are only passing parameters inward and do not require them. In the execute method, the bCachedObject flag is checked (optional) to see whether caching is desired. From here, the oSessionHeader is used to retrieve our session ID. This will be used as the key for the cache. This provides the flexibility of using different keys for each client in case each cached item should be unique to each client. This also helps with debugging in that it makes it a little easier to mimic multiple clients and/or sessions using something such as a user ID, IP address, etc. From this point, PrepareCache() is called on the ComplexReport object, passing in a newly instantiated CacheAdapter. The CacheAdapter is passed to the Context.Cache object (from the BCL), which acts as our container for this example, and the session ID key. From here, all cacheable items should be cached and the ComplexReport object created. The ICached interface is returned from the newly created ComplexReport and is used to execute the report in a "cache-sensitive" manner.

Listing 7.4 Lazy Cache Service implementation.
 [WebService(Namespace="http://www.etier.com/patterns.net")] public class LazyCacheService : System.Web.Services.WebService {    public SessionHeader oSessionHeader;    /// <summary>    /// Session Header that contains key for the cache    /// </summary>    public class SessionHeader : SoapHeader    {       public string sSessionId;    } [SoapHeaderAttribute("oSessionHeader",    Direction=SoapHeaderDirection.In, Required=false)]    [WebMethod]    public void Execute(bool bCacheObject)    {       if (bCacheObject)       {          string sKey = "defaultsessionid";          // make sure a soap header was sent.          if (oSessionHeader != null)             sKey = oSessionHeader.sSessionId;       // the cacheable object will determine if, when,       // and how to cache himself       // we just pass the container wrapper in our       // CacheAdapter and the lookup key.       ICached oCachedComplexReport = (ICached)       ComplexReport.PrepareCache(new CacheAdapter(Context.Cache), sKey);       // we are cached and prepared either way, now       // lets run          oCachedComplexReport.ExecuteCached();       }       else       {          // interact with object normally...          ComplexReport oComplexReport = new       ComplexReport();          oComplexReport.ExecuteWithoutCache();       }    } } 

The implementation of the PrepareCache method and the entire ComplexReport source follows in Listing 7.5.

Listing 7.5 Implementation sample of preparing a cache using the Abstract Cache pattern.
 /// <summary> /// This can be any cacheable business object /// </summary> public class ComplexReport : ICached, ICacheable {    // declare expensive object    byte[] m_baConfigData;    public ConcreteCacheable()    {       // perform the usual initialization    }    /// <summary>    /// Property for our configuration data    /// </summary>    public byte[] ConfigData    {       get { return m_baConfigData; }       set { m_baConfigData = value; }    }    /// <summary>    /// Factory Method for both object creation and    /// determining caching semantics (e.g. setting cache or    /// getting already cached object)    /// Our CacheAdapter is used to avoid coupling our business    /// objects with any particular cache    /// </summary>    /// <param name="oCache"></param>    /// <param name="sKey"></param>    /// <returns></returns>    public static ICached PrepareCache(CacheAdapter oCache,       string sKey)    {       ICached oCachedObject = null;    // if using the cache object check to see if this    // object is already there       // any logic could be used here to refresh the cache       // as well instead of merely checking if not object       // existing int the cache       if (oCache[sKey] == null)       {       Trace.WriteLine("PrepareCache: Cache object " +       "inserted into cache\n" +       "Current Cache Key Used =" + sKey +       "\n");          // prepare expensive config buffer          ComplexReport oCacheable = new          ComplexReport();          oCacheable.PrepareConfigData();             // add any other expensive operation here....             // ...       // now insert myself into the passed cache // // object             // this is where the business object determines       // expiration etc....       oCache.Insert(sKey, oCacheable,       DateTime.MaxValue,       TimeSpan.FromMinutes(60));       }       Trace.Write("Execute: Retrieving Item from Cache\n" +          "Cache Object Retrieved (By Key) = " +          sKey + "\n");       // just return existing cacheable object's interface    // for cached execution....       oCachedObject = (ICached)oCache[sKey];       return oCachedObject;    }    /// <summary>    /// This can be an expensive operation, db i/o, file i/o,    /// </summary>    public void PrepareConfigData()    {       FileStream oFileStream = null;       int nFileByteLength = 0;       // create and set expensive objects here       // open up our config file and read in the contents       oFileStream = new FileStream("c:\config.xml",    FileMode.OpenOrCreate, FileAccess.ReadWrite);       nFileByteLength = (int) oFileStream.Length;       ConfigData = new byte[nFileByteLength];       // save contents into config buffer       oFileStream.Read(ConfigData, 0, nFileByteLength);       // close the file       oFileStream.Close();    }    /// <summary>    /// This can be an expensive operation, db i/o, file i/o,    /// </summary>    public void PrepareWorkers()    {       // add expensive object initializations here    }    public void ExecuteCached()    {       // read in configuration data       if (ConfigData == null)          PrepareConfigData();       // use config data for something....    }    public void ExecuteWithoutCache()    {       // read in configuration data       if (ConfigData == null)          PrepareConfigData();       // use config data for something....    } } 

In the PrepareCache method, the "cacheable" object is given the opportunity to cache any data or itself, as is the case here. This is where the cacheable object determines how it will be cached and what data will be a part of that cache. It speaks strictly to the interface provided by the CacheAdapter (Listing 7.6). It first accomplishes this by calling the implemented method on the ICacheable interface methods. For our example, they are PrepareConfigData() and PrepareWorkers(). It does not matter what methods are included in the ICacheable interface, as long as they are implemented correctly for all cacheable objects. This provides the "contract" upon which the drivers of the ConcreteCacheable objects can rely. PrepareConfigData(), for example, happens to be where the expensive operation of reading configuration data from an XML file is performed. Once read, it becomes a data member (a simple byte array, for our example) of the cacheable object. Once all cached preparation methods of the ICacheable interface are called, the main PrepareCache method adds itself to be passed in CacheAdapter by calling Insert(). Here it uses its own business rules for determining the caching semantics, such as expiration. The CacheAdapter interface in our example simply wraps the main features of the System.Web.Caching.Cache object. It then delegates the Insert() call to the aggregated Cache object. If a different cache object were to be added to the framework, only the CacheAdapter would need to change. This isolates the rest of the architecture from changes in the cache container.

Listing 7.6 Wrapping a cache containerimplementation sample.
 /// <summary> /// Wraps the existing cache object container used to hold all /// cached objects, this can be Http Cache or /// any custom cache object /// </summary> public class CacheAdapter : IEnumerable {    private Cache m_oCacheContainer;    private IEnumerable m_oCacheEnum;    /// <summary>    /// Main ctor, takes most cache container e.g. Http Cache object    /// </summary>    /// <param name="oCacheContainer"></param>    public CacheAdapter(object oCacheContainer)    {       // set generic container object this will be cast for    // specific cache functionality    if (oCacheContainer is Cache) CacheContainer =    (Cache)oCacheContainer;    // set specific properties that all cached objects should    // implement       CacheEnum = (IEnumerable)oCacheContainer;    }    /// <summary>    /// Property for the main cache, should be edited is cached    /// container type is changed.    /// </summary>    private Cache CacheContainer    {       get {return m_oCacheContainer;}       set {m_oCacheContainer = value;}    }    /// <summary>    /// Property for the enumerable interface which any cache object    /// should implement    /// </summary>    private IEnumerable CacheEnum    {       get {return m_oCacheEnum;}       set {m_oCacheEnum = value;}    }    /// <summary>    /// Delegate to cache object's enumerator object to support    /// for/each, etc..    /// </summary>    public IEnumerator GetEnumerator()    {       return CacheEnum.GetEnumerator();    }    /// <summary>    /// Delegates to cache's count property    /// </summary>    public int Count    {       get {return CacheContainer.Count;}    }    /// <summary>    /// Delegates to cache's item property    /// </summary>    public object this[string sKey]    {       get {return CacheContainer[sKey];}       set {CacheContainer[sKey] = value;}    }    /// <summary>    /// Just delegates to the item property (indexer)    /// </summary>    /// <param name="sKey">see Cache object</param>    /// <returns>see Cache object</returns>    public object Get(string sKey)    {       return this[sKey];    }    /// <summary>    /// Just delegates to the insert method of the cache, most cache    /// container should implement a similar object    /// </summary>    /// <param name="sKey">see Cache object</param>    /// <param name="oValue">see Cache object</param>    /// <param name="oAbsoluteExpiration">see Cache object</param>    /// <param name="oSlidingExpiration">see Cache object</param>    public void Insert(string sKey, object oValue, DateTime       oAbsoluteExpiration, TimeSpan oSlidingExpiration)    {       CacheContainer.Insert(sKey, oValue, null,       oAbsoluteExpiration, oSlidingExpiration);    } 

Implementing a cached solution should by no means tie you to any one specific cache container or implementation. However, it is always nice to leverage code whenever possible. Using ASP.NET and its caching features is just one of those scenarios. When cacheable objects will be primarily driven from either a standard ASP.NET application or a Web service, you have the option of using the CLR and its caching container, as I did in the Abstract Cache implementation. To understand more about the System.Web.Caching.Cache object and ASP.NET caching in general, feel free to read the following technology backgrounder.

Technology BackgrounderASP.NET Caching

There are basically two forms of ASP.NET caching, one of which is called output caching (e.g., using the @OutputCache directive). I will not be covering output caching in this section. However, I will be covering the second form of caching called dynamic caching. This is the more traditional application caching you have grown to know and love (or should I say hate, in some respects). In this form of caching, you programmatically store any object into memory for the sole purpose of retrieving it at some point during execution. Objects that are cached should be expensive to create; therefore, grabbing them from memory should save you CPU cycles.

Right out of the box, ASP.NET provides you with a simple cache container that allows you to store any objects into the application server's memory. The System.Web.Caching.Cache class data type implements this container and is private to each application. This is a nondurable container, so when the machine shuts down or the application is stopped , the instance of this cache object is re-created. Any items that may reside in the cache in this situation will have to be re-created and cached once more. ASP.NET also provides a form of "scavenging" to ensure the scalability of the application by removing unimportant or seldom-used objects from the cache during low memory states.

You also have the option of controlling how long items remain in the cache by specifying an expiration date and by giving the object a priority to be used during scavenging. You use the CacheItemPriority and optionally the CacheItemPriorityDecay enumeration to set the cacheable object's priority and decay rate while calling Add() or Insert() on the container. You define the object's expiration date by using the DateTime absoluteExpiration parameter. You can also use the TimeSpan slidingExpiration parameter to allow you to specify the amount of time to elapse before the object expires , based on the time it is accessed. If an object is removed from the cache container, either through expiration or scavenging, it will be removed from the cache. Any future attempt to retrieve the object will result in a null value, and this is what you test for when checking the cache. Determining what expiration or priority to place on a cacheable object should be related to the business data being cached. This is one of the reasons (as mentioned above) for implementing the Abstract Cache pattern. It provides the standard facility to allow the business object to make its own decisions on this policy. For example, you should set the expiration value to keep cached items in the cache as long as the data in the cache remains valid; state data should always expire and recycle.

As a more advanced option, the ASP.NET cache object allows you to define the validity of a cached item based on some external dependency. This includes an external file, a directory, or another relative object in the cache. If the set dependency changes, the cached object will be removed from the cache. For example, if a file dependency is set (for a file that fed the cache's data) and that file is updated, the object will be removed from the cache. This is the perfect scenario to use for large XML files that must be read into memory and may change with regularity. Note that passing large XML files from object to object should be done with some caution, as was mentioned earlier in the book.

Adding Items to the Cache Container

There are a few ways to add objects or values to the Cache object. The Cache object is treated like a dictionary object. It is key-based, and its contents are manipulated using a key value that you provide. A key can be any unique value that will help identify the object in the cache. This can be session ID, generated token, or GUIDit doesn't matter. The key should also be remembered because it will be used to retrieve the added object from the cache. The simplest way to add an object to the cache is simply to specify the key and the object, as follows:

 Cache["Token Id"] = oCacheableObject; 

However, this doesn't provide the Cache container with much information regarding the item it just cached. To provide more control over the freshness and priority of the data being added, call Insert() or Add(). This also applies if you want to take advantage of scavenging. To specify dependencies, you must also use one of these two methods. Each has the same signature but the Add method returns an object that represents the item added. The Insert method is overloaded, providing you with a variety of ways to interact with the Cache object. For example, to simply insert an item into the Cache without specifying other parameters such as expiration, use the following:

 Cache.Insert("Token Id", oCacheableObject); 

The Add method is not overloaded; however, and you must use a more descriptive signature.

 OCacheable = (ICache)Add("Token Id", oCacheable, null,    DateTime.Now.AddMinutes(5),  NoSlidingExpiration,    priority, priorityDecay, null); 

As you can see, null values can be passed for some of the parameters whose values do not apply. Both of these methods provide you with the ability to control the caching rules and the conditions under which the item remains in the cache. The Insert method also provides the ability to add dependencies, such as the following example. This adds a dependency based on an XML file:

 Cache.Insert("Token Id", oCacheableObject, new CacheDependency(Server.MapPath(c:\Config.xml))); 

Both methods can also be used to set expiration policies for an item when you add it to the cache. You cannot set both an absolute expiration and a sliding expiration. One of the policies must be set to zero. There are two fields in the Cache class (NoAbsoluteExpiration and NoSlidingExpiration) that are used to specify the policy not being used. The following code uses the Insert method to add an item to the cache with an absolute expiration of two minutes.

 Cache.Insert("Token Id", oCacheableObject, null, DateTime.Now.AddMinutes(2),  NoSlidingExpiration); 

The following code makes use of a sliding expiration:

 Cache.Insert("Token Id", oCacheableObject, null, NoAbsoluteExpiration, TimeSpan.FromMinutes(30)); 

The priority and priorityDecay parameters are used to set the priority and decay policies, respectively. The following example uses the Add method to add an item to the cache with a priority of High and a priorityDecay of Never.

[View full width]
 
[View full width]
Cache.Add("Token Id", oCacheableObject, null, NoAbsoluteExpiration, TimeSpan.FromSeconds(30), CacheItemPriority.High, graphics/ccc.gif CacheItemPriorityDecay.Never, null);
Retrieving Cached Items from the Container

Retrieving data from the Cache is simple; you need only to specify the key and value that represent the data. Just like a dictionary object, use the key to find the cached object:

 ICached oCacheableObject; oCacheableObject = (ICached)Cache["Token Id <e.g. session id>"]; if(oCacheableObject != null) {   oCacheableObject.Execute(); } else {   oCacheableObject = new ConcreteCacheable();   Cache["Token Id"] = oCacheableObject; } 

The previous code shows how to check for whether a cache object exists in the cache by using the key. If it does not exist, a new one is created and inserted into the cache.

Removing an Item from the Cache Container

To control completely when an item is removed from the cache, you can use the Remove method. Otherwise, item "removability" is dependent on the policy rules used when adding the item, as mentioned above. For example, the following code removes an item using the "Token Id" key.

 Cache.Remove("Token Id"); 

For more information on ASP.NET caching, please refer to the documentation.

Related Patterns

  • Adapter (GoF)

  • Decorator (GoF)

  • Factory Method (GoF)



.NET Patterns. Architecture, Design, and Process
.NET Patterns: Architecture, Design, and Process
ISBN: 0321130022
EAN: 2147483647
Year: 2003
Pages: 70

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net