title: Locked Caching using Cache Keys
body: <p>We have code below we use to improve performance. It works fine, but every few days we start receiving a ton of exceptions (below). It's not related to volume, but it's random. </p>
  
  <p>Comment: /// Performs the locked code to produce the result if necessary while thread locking it and then caching the result.</p>
  
  <p>line 45 is:  lock (_keys.First(k => k == key))</p>
  
  <p>Any Ideas?</p>
  
  <p>Code:</p>
  
  <pre><code>    public class LockedCaching
  {
      private static List&lt;string&gt; _keys = new List&lt;string&gt;();
  
      public class Result
      {
          public object Value { get; set; }
          public bool ExecutedDataOperation { get; set; }
      }
  
      /// &lt;summary&gt;
      /// Performs the locked code to produce the result if necessary while thread locking it and then caching the result.
      /// &lt;/summary&gt;
      /// &lt;param name="key"&gt;&lt;/param&gt;
      /// &lt;param name="expiration"&gt;&lt;/param&gt;
      /// &lt;param name="data"&gt;&lt;/param&gt;
      /// &lt;returns&gt;&lt;/returns&gt;
      public static Result Request(string key, DateTime expiration, RequestDataOperation data)
      {
          if (key == null)
          {
              return new Result { Value = data(), ExecutedDataOperation = true };
          }
  
          //Does the key have an instance for locking yet (in our _keys list)?
          bool addedKey = false;
          bool executedDataOperation = false;
          if (!_keys.Exists(s =&gt; s == key))
          {
              _keys.Add(key);
              addedKey = true;
          }
          object ret = HttpContext.Current.Cache[key];
          if (ret == null)
          {
              lock (_keys.First(k =&gt; k == key))
              {
                  ret = HttpContext.Current.Cache[key];
                  if (ret == null)
                  {
                      ret = data();
                      executedDataOperation = true;
                      if(ret != null)
                          HttpContext.Current.Cache.Insert(key, ret, null, expiration, new TimeSpan(0));
                  }
              }
          }
          if (addedKey)
              CleanUpOldKeys();
          return new Result { Value = ret, ExecutedDataOperation = executedDataOperation };
      }
  
      private static void CleanUpOldKeys()
      {
          _keys.RemoveAll(k =&gt; HttpContext.Current.Cache[k] == null);
      }
  }
  </code></pre>
  
  <p>Exception:</p>
  
  <blockquote>
    <p>Exception: System.Web.HttpUnhandledException (0x80004005): Exception
    of type 'System.Web.HttpUnhandledException' was thrown. --->
    System.ArgumentNullException: Value cannot be null. Parameter name:
    key at System.Web.Caching.CacheInternal.DoGet(Boolean isPublic, String
    key, CacheGetOptions getOptions) at PROJECT.LockedCaching.b__8(String
    k) in PROJECT\LockedCaching.cs:line 64 at
    System.Collections.Generic.List<code>1.RemoveAll(Predicate</code>1 match) at
    PROJECT.LockedCaching.CleanUpOldKeys() in
    PROJECT\LockedCaching.cs:line 64 at
    PROJECTLockedCaching.Request(String key, DateTime expiration,
    RequestDataOperation data) in PROJECT\LockedCaching.cs:line 58 at
    FeaturesWithFlags1.DataBind() at
    System.Web.Util.CalliHelper.EventArgFunctionCaller(IntPtr fp, Object
    o, Object t, EventArgs e) at System.Web.UI.Control.LoadRecursive() at
    System.Web.UI.Control.LoadRecursive() at
    System.Web.UI.Control.LoadRecursive() at
    System.Web.UI.Control.LoadRecursive() at
    System.Web.UI.Control.LoadRecursive() at
    System.Web.UI.Control.LoadRecursive() at
    System.Web.UI.Page.ProcessRequestMain(Boolean
    includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
    at System.Web.UI.Page.HandleError(Exception e) at
    System.Web.UI.Page.ProcessRequestMain(Boolean
    includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
    at System.Web.UI.Page.ProcessRequest(Boolean
    includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
    at System.Web.UI.Page.ProcessRequest() at
    System.Web.UI.Page.ProcessRequest(HttpContext context) at
    System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
    at System.Web.HttpApplication.ExecuteStep(IExecutionStep step,
    Boolean&amp; completedSynchronously)</p>
  </blockquote>
  
  <p>Web control where it's used - This web control requests a list of locations from a webservice. We use this lockedcache request almost everywhere we call the webservice.:</p>
  
  <pre><code>public override void DataBind()
  {
      try
      {
          string cacheKey = "GetSites|";
          mt_site_config[] sites = (mt_site_config[])LockedCaching.Request(cacheKey, DateTime.UtcNow.AddMinutes(10),
          () =&gt;
          {
              WebServiceClient service = new WebServiceClient();
              sites = service.GetSites();
              service.Close();
              return sites;
          }).Value;
          ddlLocation.Items.Clear();
          ddlLocation.Items.Add(new ListItem("Please Select"));
          ddlLocation.Items.Add(new ListItem("Administration"));
          ddlLocation.Items.AddRange
          (
              sites.Select
              (
                  s =&gt; new ListItem(s.site_name + " " + s.site_location, s.th_code.ToString())
              ).ToArray()
          );
      }
      catch (Exception ex) {
          Logger.Error("ContactUs Control Exception: Exp" + Environment.NewLine + ex.Message);
      }
      base.DataBind();
  </code></pre>
  
  <p>}</p>
  
  <p>Thank you for your comments. ConcurrentDictionary was the way to go. The issue to why we received errors was because the linq code "lock (_keys.First(k => k == key))" was returning an exception rather than null. Using the concurrentdictionary will be much safer and hopefully not cause any lock issues.</p>
  
  <p>Modified Code:</p>
  
  <pre><code>public class LockedCaching
  {
  
      public class Result
      {
          public object Value { get; set; }
          public bool ExecutedDataOperation { get; set; }
      }
  
      public static Result Request(string key, DateTime expiration, RequestDataOperation data)
      {
          if (key == null)
          {
              return new Result { Value = data(), ExecutedDataOperation = true };
          }
  
          object results = HttpContext.Current.Cache[key];
          bool executedDataOperation = false;
  
          if (results == null)
          {
              object miniLock = _miniLocks.GetOrAdd(key, k =&gt; new object());
              lock (miniLock)
              {
                  results = HttpContext.Current.Cache[key];
                  if (results == null)
                  {
                      results = data();
                      executedDataOperation = true;
                      if (results != null)
                          HttpContext.Current.Cache.Insert(key, results, null, expiration, new TimeSpan(0));
  
                      object temp;
                      object tempResults;
                      if (_miniLocks.TryGetValue(key, out temp) &amp;&amp; (temp == miniLock))
                          _miniLocks.TryRemove(key, out tempResults);
  
                  }
              }
          }
          return new Result { Value = results, ExecutedDataOperation = executedDataOperation };
      }
  
      private static readonly ConcurrentDictionary&lt;string, object&gt; _miniLocks =
                                new ConcurrentDictionary&lt;string, object&gt;();
  
  }
  </code></pre>