views:

1319

answers:

7

For instance I have a BaseController that I want all Controllers to inherit from. Within there I want to have a User property that will simply grab the User data from the database so that I can use it within the controller, pass it to the views, stuff like that.

Code would look something like:

protected User GetUser(int id)
{
  return _repository.Get<User>(id);
}

All fine and dandy, but now I want to cache this. I'm using this information on every single page so there is no need to go to the database each page request.

I'd like something like:

if(_user is null)
  GrabFromDatabase
  StuffIntoCache
return CachedObject as User

Make sense? Or is there a better way to do this? TIA

+1  A: 

If you don't need specific invalidation features of ASP.NET caching, static fields are pretty good, lightweight and easy to use. However, as soon as you needed the advanced features, you can switch to ASP.NET's Cache object for storage.

The approach I use is to create a property and a private field. If the field is null, the property will fill it and return it. I also provide an InvalidateCache method that manually sets the field to null. The advantage of this approach it that the caching mechanism is encapsulated in the property and you can switch to a different approach if you want.

Mehrdad Afshari
I static field will persist across page views?
rball
Yes, it'll persist across the AppDomain (side note: or thread if you specify [ThreadStatic], which you do not want.)
Mehrdad Afshari
+1  A: 

If you want it cached for the length of the request, put this in your controller base class:

public User User {
    get {
     User _user = ControllerContext.HttpContext.Items["user"] as User;

     if (_user == null) {
      _user = _repository.Get<User>(id);
      ControllerContext.HttpContext.Items["user"] = _user;
     }

     return _user;
    }
}

If you want to cache for longer, use the replace the ControllerContext call with one to Cache[]. If you do choose to use the Cache object to cache longer, you'll need to use a unique cache key as it will be shared across requests/users.

John Sheehan
I'll try this out when I get home. Thanks!
rball
+9  A: 

You can still use the cache (shared among all responses) and session (unique per user) for storage.

I like the following "try get from cache/create and store" pattern (c#-like pseudocode):

public class CacheExtensions
{
  public static T GetOrStore<T>(this Cache cache, string key, Func<T> generator)
  {
    var result = cache[key];
    if(result == null)
    {
      result = generator();
      cache[key] = result;
    }
    return (T)result;
  }
}

you'd use this like so:

var user = HttpRuntime
              .Cache
              .GetOrStore<User>(
                 string.Format("User{0}", _userId), 
                 () => Repository.GetUser(_userId));

You can adapt this pattern to the Session, ViewState (ugh) or any other cache mechanism. You can also extend the ControllerContext.HttpContext (which I think is one of the wrappers in System.Web.Extensions), or create a new class to do it with some room for mocking the cache.

Will
don't use cache[key]=result but Cache.Insert(...) since you can put dependency(ies), expiration policy and so on in Insert.
Andrei Rinea
Why I didn't think to use Session is beyond me. I think that would solve the problem pretty nicely.
rball
+1 to Andrei. That was "c#-like pseudocode", by which I mean I wrote it from memory and isn't necessarily the best code or bug-free.
Will
+2  A: 

I like to hide the fact that the data is cached in the repository. You can access the cache through the HttpContext.Current.Cache property and store the User information using "User"+id.ToString() as the key.

This means that all access to the User data from the repository will use cached data if available and requires no code changes in the model, controller, or view.

I have used this method to correct serious performance problems on a system that was querying the database for each User property and reduced page load times from minutes to single digit seconds.

Matthew
+1 Or even better, use a decorator repository that caches the results of a real one
Richard Szalay
+9  A: 

Will, your CacheExtensions class should be declared static. I'd also like to suggest a slight alteration in order to deal with the possibility of Func being a null reference

public static class CacheExtensions
{

    private static object sync = new object();
    public const int DefaultCacheExpiration = 20;

    /// <summary>
    /// Allows Caching of typed data
    /// </summary>
    /// <example><![CDATA[
    /// var user = HttpRuntime
    ///   .Cache
    ///   .GetOrStore<User>(
    ///      string.Format("User{0}", _userId), 
    ///      () => Repository.GetUser(_userId));
    ///
    /// ]]></example>
    /// <typeparam name="T"></typeparam>
    /// <param name="cache">calling object</param>
    /// <param name="key">Cache key</param>
    /// <param name="generator">Func that returns the object to store in cache</param>
    /// <returns></returns>
    /// <remarks>Uses a default cache expiration period as defined in <see cref="CacheExtensions.DefaultCacheExpiration"/></remarks>
    public static T GetOrStore<T>( this Cache cache, string key, Func<T> generator ) {
        return cache.GetOrStore( key, generator != null ? generator() : default( T ), DefaultCacheExpiration );
    }


    /// <summary>
    /// Allows Caching of typed data
    /// </summary>
    /// <example><![CDATA[
    /// var user = HttpRuntime
    ///   .Cache
    ///   .GetOrStore<User>(
    ///      string.Format("User{0}", _userId), 
    ///      () => Repository.GetUser(_userId));
    ///
    /// ]]></example>
    /// <typeparam name="T"></typeparam>
    /// <param name="cache">calling object</param>
    /// <param name="key">Cache key</param>
    /// <param name="generator">Func that returns the object to store in cache</param>
    /// <param name="expireInMinutes">Time to expire cache in minutes</param>
    /// <returns></returns>
    public static T GetOrStore<T>( this Cache cache, string key, Func<T> generator, double expireInMinutes ) {
        return cache.GetOrStore( key, generator != null ? generator() : default( T ), expireInMinutes );
    }


    /// <summary>
    /// Allows Caching of typed data
    /// </summary>
    /// <example><![CDATA[
    /// var user = HttpRuntime
    ///   .Cache
    ///   .GetOrStore<User>(
    ///      string.Format("User{0}", _userId),_userId));
    ///
    /// ]]></example>
    /// <typeparam name="T"></typeparam>
    /// <param name="cache">calling object</param>
    /// <param name="key">Cache key</param>
    /// <param name="obj">Object to store in cache</param>
    /// <returns></returns>
    /// <remarks>Uses a default cache expiration period as defined in <see cref="CacheExtensions.DefaultCacheExpiration"/></remarks>
    public static T GetOrStore<T>( this Cache cache, string key, T obj ) {
        return cache.GetOrStore( key, obj, DefaultCacheExpiration );
    }

    /// <summary>
    /// Allows Caching of typed data
    /// </summary>
    /// <example><![CDATA[
    /// var user = HttpRuntime
    ///   .Cache
    ///   .GetOrStore<User>(
    ///      string.Format("User{0}", _userId), 
    ///      () => Repository.GetUser(_userId));
    ///
    /// ]]></example>
    /// <typeparam name="T"></typeparam>
    /// <param name="cache">calling object</param>
    /// <param name="key">Cache key</param>
    /// <param name="obj">Object to store in cache</param>
    /// <param name="expireInMinutes">Time to expire cache in minutes</param>
    /// <returns></returns>
    public static T GetOrStore<T>( this Cache cache, string key, T obj, double expireInMinutes ) {
        var result = cache[key];

        if ( result == null ) {

            lock ( sync ) {
                if ( result == null ) {
                    result = obj != null ? obj : default( T );
                    cache.Insert( key, result, null, DateTime.Now.AddMinutes( expireInMinutes ), Cache.NoSlidingExpiration );
                }
            }
        }

        return (T)result;

    }

}

I would also consider taking this a step further to implement a testable Session solution that extends the System.Web.HttpSessionStateBase abstract class.

public static class SessionExtension
{
    /// <summary>
    /// 
    /// </summary>
    /// <example><![CDATA[
    /// var user = HttpContext
    ///   .Session
    ///   .GetOrStore<User>(
    ///      string.Format("User{0}", _userId), 
    ///      () => Repository.GetUser(_userId));
    ///
    /// ]]></example>
    /// <typeparam name="T"></typeparam>
    /// <param name="cache"></param>
    /// <param name="key"></param>
    /// <param name="generator"></param>
    /// <returns></returns>
    public static T GetOrStore<T>( this HttpSessionStateBase session, string name, Func<T> generator ) {

        var result = session[name];
        if ( result != null )
            return (T)result;

        result = generator != null ? generator() : default( T );
        session.Add( name, result );
        return (T)result;
    }

}
njappboy
Nice addition, I like it
rball
Thx rball. Happy coding =)
njappboy
A: 

@njappboy: Nice implementation. I would only defer the Generator( ) invocation until the last responsible moment. thus you can cache method invocations too.

/// <summary>
/// Allows Caching of typed data
/// </summary>
/// <example><![CDATA[
/// var user = HttpRuntime
///   .Cache
///   .GetOrStore<User>(
///      string.Format("User{0}", _userId), 
///      () => Repository.GetUser(_userId));
///
/// ]]></example>
/// <typeparam name="T"></typeparam>
/// <param name="Cache">calling object</param>
/// <param name="Key">Cache key</param>
/// <param name="Generator">Func that returns the object to store in cache</param>
/// <returns></returns>
/// <remarks>Uses a default cache expiration period as defined in <see cref="CacheExtensions.DefaultCacheExpiration"/></remarks>
public static T GetOrStore<T>( this Cache Cache, string Key, Func<T> Generator )
{
    return Cache.GetOrStore( Key, Generator, DefaultCacheExpiration );
}

/// <summary>
/// Allows Caching of typed data
/// </summary>
/// <example><![CDATA[
/// var user = HttpRuntime
///   .Cache
///   .GetOrStore<User>(
///      string.Format("User{0}", _userId), 
///      () => Repository.GetUser(_userId));
///
/// ]]></example>
/// <typeparam name="T"></typeparam>
/// <param name="Cache">calling object</param>
/// <param name="Key">Cache key</param>
/// <param name="Generator">Func that returns the object to store in cache</param>
/// <param name="ExpireInMinutes">Time to expire cache in minutes</param>
/// <returns></returns>
public static T GetOrStore<T>( this Cache Cache, string Key, Func<T> Generator, double ExpireInMinutes )
{
    var Result = Cache [ Key ];

    if( Result == null )
    {
        lock( Sync )
        {
            if( Result == null )
            {
                Result = Generator( );
                Cache.Insert( Key, Result, null, DateTime.Now.AddMinutes( ExpireInMinutes ), Cache.NoSlidingExpiration );
            }
        }
    }

    return ( T ) Result;
}
SDReyes
A: 

@njappboy

I think there is a bug in ur code.. the function recalled even the cache is exist!

simply change "generator != null " into "(cache[key] == null && generator != null)" to fix the bug..

public static T GetOrStore<T>( this Cache cache, string key, Func<T> generator ) {
    return cache.GetOrStore( key, (cache[key] == null && generator != null) ? generator() : default( T ), DefaultCacheExpiration );
}

public static T GetOrStore<T>( this Cache cache, string key, Func<T> generator, double expireInMinutes ) {
    return cache.GetOrStore( key, (cache[key] == null && generator != null) ? generator() : default( T ), expireInMinutes );
}
TeYoU