Wednesday, July 22, 2009

Caching HTML helpers on ASP.net MVC with optional SQL Cache Dependency

When thinking about performance, one of the top things that comes in mind is caching. Caching means that you will save some server resources by saving the result on the “first” time it’s processed. With ASP.net MVC, it’s common the use of HTML Helpers, that are just methods that return a resulting HTML. We can combine both and cache the resulting HTML on the first time the method is called, round house kicking the performance of the page.

To archive this, we first create a CacheExtensions class to hold our caching methods:

public static class CacheExtensions
{
   public static bool TryGet (this Cache cache, string key, out T value)
   {
       var obj = cache.Get (key);

       if (obj != null)
       {
           value = (T)obj;
           return true;
       }

       value = default(T);

       return false;
   }

   public static void CacheData (this Cache cache, string key, string profileKey, T data)
   {
       var dc = DataCacheSettings.GetDataCacheProfile (profileKey);

       if (dc == null)
           throw new ArgumentException ("Data cache profile {0} not found".FormatWith (profileKey));

       if (data.Equals(default(T)))
           return;

       if (dc.Enabled)
       {
           if (dc.UseDependency)
           {

               if (string.IsNullOrEmpty(dc.SqlDependency))
                   throw new InvalidOperationException("sqlDependency must be set when dependency is enabled.");

               var sqlDepInfo = dc.SqlDependency.Split(new char[] {':'}, StringSplitOptions.RemoveEmptyEntries);

               if (sqlDepInfo.Length != 2)
                   throw new InvalidOperationException("sqlDependency option must obey the Database:Table format");

               cache.Insert (key,
                             data,
                             new SqlCacheDependency(sqlDepInfo[0], sqlDepInfo[1]),
                             Cache.NoAbsoluteExpiration,
                             TimeSpan.FromMinutes(dc.Duration),
                             dc.Priority,
                             null);
           }
           else
           {
               cache.Insert (key,
                             data,
                             null,
                             Cache.NoAbsoluteExpiration,
                             TimeSpan.FromMinutes (dc.Duration),
                             dc.Priority,
                             null);
           }
       }
   }
}

The TryGet method will check if the cache key exists and assign it to the out value, returning true if there was a matching key and false if the cache didn’t exist.

The CacheData method caches the data by the provided key. This method supports cache profiles and SQL Server Cache Dependency which i will explain later on this same article.

To use it:

public static class HtmlHelperExtensionsWithCache
{
   public static string Hello(this HtmlHelper helper, string name)
   {
       string key = String.Concat("hello-", name);
       string ret;

       if (Cache.TryGet(key, out ret))
           return ret;

       //if you have to format some html, use StringBuilder
       ret = String.Format("Hello {0}, have a nice day!", name);

       Cache.CacheData(key, "Html", ret);

       return ret;
   }
}

This extension method will act as a HTML helper and cache the message depending on the user name. The “Html” parameter of the CacheData method will read the “Html” cache profile from web.config, which is configured this way (for example):

<datacachesettings>
   <profiles>
   <!-- duration in minutes -->
       <add priority="High" duration="30" name="Html" />
       <add priority="Normal" duration="10" name="User" />
       <add priority="AboveNormal" duration="15" name="StoryList" usedependency="true" sqldependency="DotNetBurner:Story" />
       <add priority="BelowNormal" duration="15" name="VoteButton" />
       <add priority="Normal" duration="60" name="Planet" />
       <add priority="Low" duration="1440" name="StaticResource" />
   </profiles>
</datacachesettings>

Configuration Properties:

name: The profile name. On this case we used “Html”; duration: The duration, in minutes; priority: The cache priority (the same used on ASP.net output cache configuration). Lower priorities will likely be invalidated first. usedependency: If SQL dependency is enabled; sqldependency: Folows the format <dependency configuration name>:<table name> where dependency configuration name is also configured on web.config, as follows:

<sqlcachedependency enabled="true" polltime="5000">
   <databases>
       <add name="DotNetBurner" connectionstringname="DotNetBurnerConnectionString" />
   </databases>
</sqlcachedependency>

This will check the database every 5 seconds. If the table changed, the profile is invalidated. This means that every cache that depends on this profile will be invalidated.

This is used on DotNetBurner and it’s working very well so far. I used this caching technique where i could (except on user dependent data) and the site is very fast, even for Chuck Norris.

No comments:

Post a Comment