An alternative way to cache UserControls in Asp.net

    I think everyone who uses Asp.net to develop web sites is well aware that Asp.net has built-in caching for UserControls.
    Any user element can be cached for a certain time depending on various conditions. such a cache works extremely fast and in most cases this is quite enough, but this turned out to be not enough in projects in which I take part.
    The main disadvantages were as follows:
    • The cache may depend not only on the parameters of the query string, but also on some other parameters, for example, cookies or the type of user (for example, in the online store this can be an individual or legal entity)
    • The entire cache should not disappear when recompiling the project (this usually happens when changing the web.config configuration file)
    • If necessary, you need to be able to clear the entire cache

    Initially, we periodically generated a cache for the entire site, but over time it became a very big problem due to the complexity of the controls, so I decided to look for another solution.

    Its essence is to create a control that would know how to cache itself.
    To do this, take the UserControl class and create the CacheUserControl class derived from it:
    public abstract class CacheUserControl : UserControl
    {
      public abstract string GetCachePath();
      public abstract TimeSpan GetCacheTime();

      public bool IsCache { get; protected set; }

      protected string cacheText;
      protected string cachePath;
      protected sealed override void OnInit(EventArgs e)
      {
        ...
      }
      public sealed override void RenderControl(HtmlTextWriter htmlTextWriter)
      {
        ...
      }
    }


    As you can see, there are two abstract methods.
    The GetCachePath method returns the path to the cache file. This path should reflect all the dependencies of the control so that it can select the correct file.
    The GetCacheTime method returns the time interval after which the cache will be considered obsolete and will be updated if necessary.

    Next, you need to redefine the two methods of the UserControl class:

    The OnInit method must check whether the file with the cache exists, and if so, the control must delete all its child controls so that no events are raised in them and load the cache from the file:

    protected sealed override void OnInit(EventArgs e)
    {
      try
      {
        cachePath = GetCachePath();
        if (cachePath != null)
        {
          var cacheFile = new FileInfo(cachePath);
          if (cacheFile.Exists)
          {
            var cacheTime = GetCacheTime();

            var cacheTimeExpired = cacheFile.CreationTime + cacheTime < DateTime.Now;
            if (!cacheTimeExpired)
            {
              using (var reader = cacheFile.OpenText())
              {
                cacheText = reader.ReadToEnd();
                IsCache = true;
                Controls.Clear();
              }
            }
          }
        }
      }
      catch (IOException)
      {
      }

      base.OnInit(e);
    }


    The RenderControl method should check whether the cache is loaded, and if it is loaded, then render it, and if not, then render the control in normal mode and additionally create a file with a cache:

    public sealed override void RenderControl(HtmlTextWriter htmlTextWriter)
    {
      if(IsCache)
      {
        htmlTextWriter.Write(cacheText);
      }
      else
      {
        base.RenderControl(htmlTextWriter);

        try
        {
          if (cachePath != null)
          {
            using (var streamWriter = new StreamWriter(cachePath, false, Encoding.UTF8))
            {
              using (htmlTextWriter = new HtmlTextWriter(streamWriter))
              {
                base.RenderControl(htmlTextWriter);
              }
            }
          }
        }
        catch (IOException)
        {
        }
      }
    }


    As a result, to use the self-caching control, you need to change the base class from UserControl to CacheUserConrtol and additionally define two methods, for example like this:

    public override string GetCachePath()
    {
      var id = Request.QueryString["id"];
      if(string.IsNullOrEmpty(id)) return null;

      var currency = "rur";
      var cookie = Request.Cookies["currency"];
      if(cookie != null)
      {
        var value = cookie.Value;
        if (value == "usd") currency = "usd";
        else if (value == "euro") currency = "euro";
      }

      var path = string.Format("~/cache/cat_{0}_cur_{1}.html", id, currency);
      return Server.MapPath(path);
    }
    public override TimeSpan GetCacheTime()
    {
      return TimeSpan.FromDays(7);
    }


    Now the control itself will create separate files with a cache for various categories (determined by id) for each currency of the catalog (determined from cookie)

    The only minus of this approach is that over time garbage will accumulate in the cache, so it will have to be completely cleaned periodically. It should also be noted that the Page_Load method of such a control is called in any case, but the events of child controls only if the content is not taken from a file.

    Well, that's all.


    Also popular now: