Elegant ASP.NET Caching

So you have a central method that returns some common data, say for example US States.

public IList<State> GetUsStates()

{

    List<State> states = new List<State>();

    using(IDataReader dr = DataAccess.ExecuteReader("SELECT * FROM US_STATES"))

    {

        State theState = State.Fetch(dr);

        states.Add(theState);

    }

 

    return states;

}

Pretty simple stuff, loop over a datareader, building “State” objects based on a row in the database.

Well since this data hardly ever changes and is the same for every user, it is a prime target for caching.  (Psst, if you didn’t know, caching can be one of the most effective performance tuning techniques you can perform — if done correctly).

Now that we want to enable caching, we can create a little helper class that makes it a bit easier to work with…

This is a watered down version of the cache API, but it solves 90% of the cases.

public class CacheHelper

{

    public static bool ItemExists(string key)

    {

        return HttpContext.Current.Cache[key] != null;

    }

 

    public static void Insert(string key, object obj)

    {

        TimeSpan oneHour = new TimeSpan(1,0,0);

        Insert(key, obj, oneHour);

    }

 

    public static void Insert(string key, object obj, TimeSpan span)

    {

        HttpContext.Current.Cache.Add(

            key,

            obj,

            null, //no dependencies

            DateTime.Now.Add(span),

            Cache.NoSlidingExpiration,

            CacheItemPriority.Normal,

            null //no remove callback

        );

    }

 

    public static T Retrieve<T>(string key)

    {

        return (T)HttpContext.Current.Cache[key];

    }

}

Now that we have the utility methods in place, we can alter our US States function to enable caching:

public IList<State> GetUsStates()

{

    string statesKey = "US_STATES";

    if (!CacheHelper.ItemExists(statesKey))

    {

        //add it

        List<State> states = new List<State>();

        using (IDataReader dr = DataAccess.ExecuteReader("SELECT * FROM US_STATES"))

        {

            State theState = State.Fetch(dr);

            states.Add(theState);

        }

 

        CacheHelper.Insert(statesKey, states);

    }

 

    //now it definitely exists in cache

    return CacheHelper.Retrieve<IList<State>>(statesKey);

}

This code quickly gets repetetive and ugly, but it gets the job done.  How can we do better?

We need to be able to provide the means of getting the data, but without actually executing it every time.  This sounds like a good use for a delegate!

In our CacheHelper class we define the delegate:

 public delegate object RetrieveDelegate();

It’s just a method signature that returns an object.

Then we define another method that will first check the cache, and execute the delegate if the data is not there:

public static T GetAndCache(string key, RetrieveDelegate retrieveObject, TimeSpan span)

{

    if (!ItemExists(key))

    {

        object value = retrieveObject(); //this makes the call to the database

        Insert(key, value, span);

    }

 

    return Retrieve<T>(key);

}

Now we have a method that can be smart about caching, but it is completely agnostic of what data to cache.  Now we can refactor our GetStates() method to this:

public IList<State> GetUsStates()

{

    return CacheHelper.GetAndCache<IList<State>>(

        "US_STATES",

        delegate {

            List<State> states = new List<State>();

            using (IDataReader dr = DataAccess.ExecuteReader("SELECT * FROM US_STATES"))

            {

                State theState = State.Fetch(dr);

                states.Add(theState);

            }

            return states;

        },

        TimeSpan.FromDays(1));

}

We only have to interact with the CacheHelper once, thus we only use the cache key once as well.  There are no if statements here either.  Just a single function call that excepts an anonymous method.

This is pretty clean and it makes caching in your application easier.  What do you think?  Do you have any ideas for improvement?  Let’s hear in the comments!

#1 mnichols AT cei-az.com (Mike Nichols) avatar
mnichols AT cei-az.com (Mike Nichols)
8.17.2007
3:19 AM

It seems like you are violating Separation of Concerns here. First, you are having your method get the cache mechanism. I'd rather inject an ICache that the method consumes (perhaps at class level, not method parameter). I guess you could do the injection into the CacheHelper, though so that slips by that barely. Also, the actual access code should be in its own object as well rather than here I think since that is another mix of concerns here. If you encapsulated it in an object you could mark the object as ICacheable or something to be passed to a ChainOfResponsibility handler that caches the results once it has accessed the cache.Finally, in my opinion you are blending a dataaccess concern into your cache even though it is wrapped up in a delegate. Abstracting the conditional in a delegate seems to obscure the code to me. Chain Of Responsibility or perhaps decorating the class that has this method would be my first go at it.Just my 2,004 pesos. :)


#2 Ben Scheirman avatar
Ben Scheirman
8.17.2007
9:28 AM

The data access portion, I agree is poor, however to keep th example simple I wanted to keep that part straight forward and focus on what I really wanted to do:Cache the item without convoluting the intent of the method.The code is still 90% of (How do I get the US STATES?) but I have added an extra couple lines for caching.This is better than the first case.But I do agree with you that I should be able to separate the caching completely from the data access, so maybe I'll use the decorator pattern to accomplish it.public class CommonDataService : ICommonDataService{public IList<State> GetStates() { .. } //as normal}public class CachingCommonDataServiceDecorator : ICommonDataService{ ICommonDataService _innerService; public CachingCommonDataServiceDecorator(ICommonDataService innerService) {_innerService = innerService; } public IList<State> GetStates() {return CacheHelper.GetAndCache<IList<State>>("US_STATES",delegate { return _innerService.GetStates(); },TimeSpan.FromDays(1));}}I like this better.More improvements?


#3 Jeffrey Palermo avatar
Jeffrey Palermo
8.18.2007
1:03 AM

Ben,Your example in the comment is much better.This separates the concerns, and your first implementation has only 1 reason to change: data access changes.Your caching implementation only has 1 reason to change, different caching strategy.I would rename your CachingCommonDataServiceDecorator to WebRequestCachedCommonDataServiceProxy.First, you are using HttpContext, so this class can only operate within a web request. Second (assuming I'm not blatantly wrong), this is more of a Proxy pattern than a Decorator pattern.Decorator observes and modifies input before passing it on.Proxies have the option of not passing it on (such as when it's a cache hit).


#4 Ben Scheirman avatar
Ben Scheirman
8.18.2007
1:32 AM

Thanks for the input Jeffrey.Pulling out my GoF book (and dusting it off) I see that you're right about the naming.The patterns are very similar though.As far as the Web naming, I actually wouldn't reference HttpContext there, I would abstract that as well, so that I could use EntLib caching on WinForms and HttpContext.Current.Cache for Webforms.I left that out for sake of simplicity though.


#5 Jared314 avatar
Jared314
8.25.2007
4:16 PM

You might also need a way to access the value without caching, for administration or accuracy. If you store the delegate to a local variable you could call the cache helper conditionally, based on a boolean parameter. To maintain the interface, you would need to overload the the function with the default behavior you wanted.


#6 Jared314 avatar
Jared314
8.25.2007
6:34 PM

Please ignore my last post, I missed the CommonDataService in your example. My scenario is solved by that extra layer.