Showing posts with label Caching. Show all posts
Showing posts with label Caching. Show all posts

Tuesday, August 11, 2009

Lazy loading a database query to improve performance of caching on ASP.net MVC

This article explains an approach to lazy load a database query on an ASP.net MVC controller to prevent the query from executing on it, and just execute on the View, if it's not cached. So, if the View is cached, the database is not queried.

With ASP.net MVC you would obviously have a Model, a View and a Controller. But the workflow of a page execution on MVC is not really M-V-C, because it doesn't happens on this order. Skipping a few steps, to keep the explanation simpler, let's start with the Controller. On the controller it's common to query a database on the Model and set the View model with the data. On the view, you usually have a Html helper that will take the view model and render it as Html.

There lies a problem. If you use caching on your HTML helper, the database is still queried, because the query would be issued on the Controller, which happens before the View (which is cached).

To understand this, I recommend you to take a look on a previous article: http://tsayao.blogspot.com/2009/07/caching-html-helpers-on-aspnet-mvc.html.

The solution is pretty simple: Lazy load the data on the Controller.

This way, the database is not queried on the Controller, it's just lazy loaded when the Enumeration is requested at the View. So, if the Html helper on the View is cached, it's never requested!

First, let's implement a simple LazyColletion class:

using System.Collections;
using System.Collections.Generic;

namespace DotNetBurner
{
  public class LazyCollection<t> : IEnumerable<t>
  {
      public delegate IEnumerable<t> LazyCallback();
      private readonly LazyCallback _callBack;
      private IEnumerable<t> _items;
      private bool _wasCalled = false;

      public LazyCollection(LazyCallback callBack)
      {
          _callBack = callBack;
      }

      IEnumerator IEnumerable.GetEnumerator()
      {
          CallCallBack();
          return _items.GetEnumerator();
      }

      public IEnumerator<t> GetEnumerator()
      {
          CallCallBack();
          return _items.GetEnumerator();
      }

      void CallCallBack()
      {
          lock (this)
          {
              if (!_wasCalled)
                  _items = _callBack();

              _wasCalled = true;
          }
      }
  }
}

The limitation of this example is that your database query method should return a collection that implements IEnumerable<T>. Something like this:

  
public virtual IEnumerable<Tag> GetTags(int top)
{
  List<Tag> tags = new List<Tag>();

  using (SqlConnection conn = new SqlConnection(_connStr))
  {
      using (SqlCommand cmd = new SqlCommand("sp_tag_get_tops", conn))
      {
          cmd.CommandType = CommandType.StoredProcedure;
          cmd.Parameters.AddWithValue("top", top);
          conn.Open();

          using (SqlDataReader reader = cmd.ExecuteReader())
          {
              while (reader.Read())
              {
                  Tag tag = new Tag
                                {
                                    StoryCount = reader.GetInt32("Count"),
                                    TagId = reader.GetInt32("TagId"),
                                    Name = reader.GetString("Name")
                                };

                  tags.Add(tag);
              }
          }
      }
  }

  return tags;
}  

On the Controller, you can do something like this:

viewData.Tags = new LazyCollection<Tag>(() => Repository.GetTags(StorySettings.Settings.TopTags));

This way, the database is just queried when you use the View Model. On this case, if you never enumerate ViewData.Model.Tags (it happens when you cache the view part that touches it), no query is issued on the database. This example uses the SqlClient directly, but i think you can extend it to use Linq to Entities.

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.