SharePoint caching and CacheDependency

Search

Accessible SharePoint WebSites
Download ARF

SharePoint caching and CacheDependency

http://blog.thekid.me.uk

One of the things that can really help the performance of your site is caching. Going to the SharePoint database for every request to retrieve a file can be very expensive, particularly if once the file has been retrieved you need to process it further. Its a good idea to cache that file (or the processed results), using the standard System.Web.Caching.Cache.

HttpContext.Cache

The HttpContext.Cache property gives you easy access to the current cached objects. The collection of cached objects are stored using a string as a key and generally you would add your file using code similar to the following...

HttpContext.Current.Cache.Add("YourKey",

                              oYourSharePointFile,

                              null,

                              DateTime.Now.AddMinutes(2),

                              System.Web.Caching.Cache.NoSlidingExpiration,

                              System.Web.Caching.CacheItemPriority.Normal, null);

Once added you can access your cached object using...

string sFileContents = (string)HttpContext.Current.Cache["YourKey"];

This saves you having to go to SharePoint again to retrieve the file contents for every request. The problem is that you will have to load it again in two minutes, and if the file changes within that two minutes they will not be reflected. Also if the file hasn't changed you are re-loading it even though you don't need to.

System.Web.Caching.CacheDependency

This is where the CacheDependency class helps you out as it allows you to invalidate the cache only when the file changes. This allows you to change the code to the following...

HttpContext.Current.Cache.Add("YourKey",

                              oYourSharePointFile,

                              new SPFileCacheDependency(SPContext.Current.Site.Url, XslFullPath),

                              System.Web.Caching.Cache.NoAbsoluteExpiration,

                              System.Web.Caching.Cache.NoSlidingExpiration,

                              System.Web.Caching.CacheItemPriority.Normal, null);

Now the cached SharePoint file will only be removed when it is invalidated by the CacheDependency, not after a set period of time, the only problem being that SharePoint does not provide an implementation of this class for you to use.

In .Net you not only have the CacheDependency, which allows you to monitor files, but also the SqlCacheDependency, which allows you to check for database changes. Unfortunately neither of these classes help us with SharePoint files and so we need to provide our own implementation.

Writing your own SPFileCacheDependency class

In order to provide our own version we need to write a class which inherits from CacheDependency...

public class SPFileCacheDependency : CacheDependency

{

    private string _uniqueID = Guid.NewGuid().ToString();

    private string _siteUrl;

    private string _itemUrl;

 

    public void RaiseChangedEvent(object sender, SPFileCacheEventArgs e)

    {

        base.NotifyDependencyChanged(sender, e);

    }

 

    public SPFileCacheDependency(string siteUrl, string fileUrl)

    {

        _itemUrl = fileUrl.ToLower();

        _siteUrl = siteUrl.ToLower();

        SPFileWatcher.Current(siteUrl).AddDepedentItem(this, fileUrl);

    }

 

    public override string GetUniqueID()

    {

        return _uniqueID;

    }

 

    protected override void DependencyDispose()

    {

        try

        {

            SPFileWatcher.Current(_siteUrl).RemoveDepedentItem(this, _itemUrl);

 

            base.DependencyDispose();

        }

        catch (Exception ex)

        {

            Log.Trace(ex);

        }

    }

}

This class overrides the important methods in order to manage the dependency, it uses another class (SPFileWatcher) to actually manage the process of checking the files. This class uses a timer to periodically check the files to see if they have changed. The implementation of SPFileWatcher is below...

public class SPFileWatcher

{

    class ItemDetails

    {

        public DateTime LastModified;

        public List<SPFileCacheDependency> Dependencies = new List<SPFileCacheDependency>();

    }

 

    private static SPFileWatcher _current;

    private string _siteUrl;

 

    private Timer _timer;

    private Dictionary<string, ItemDetails> _items;

 

    public SPFileWatcher(string siteUrl)

    {

        _items = new Dictionary<string, ItemDetails>();

        _siteUrl = siteUrl.ToLower();

    }

 

    private Dictionary<string, ItemDetails> Items

    {

        get { return _items; }

        set { _items = value; }

    }

 

    public static SPFileWatcher Current(string siteUrl)

    {

        if (_current == null)

            _current = new SPFileWatcher(siteUrl);

 

        return _current;

    }

 

    public void AddDepedentItem(SPFileCacheDependency owner, string urlOfItem)

    {

        try

        {

            if (!Items.ContainsKey(urlOfItem.ToLower()))

            {

                ItemDetails details = new ItemDetails();

                details.LastModified = DateTime.UtcNow;

                details.Dependencies.Add(owner);

                Items.Add(urlOfItem.ToLower(), details);

            }

            else

            {

                ItemDetails details = Items[urlOfItem.ToLower()];

                details.Dependencies.Add(owner);

            }

 

            EnsureStarted();

        }

        catch (Exception ex)

        {

            System.Diagnostics.Trace.WriteLine(ex);

        }

    }

 

    public void RemoveDepedentItem(SPFileCacheDependency owner, string urlOfItem)

    {

        if (!Items.ContainsKey(urlOfItem)) return;

 

        ItemDetails details = Items[urlOfItem];

        if (details.Dependencies.Contains(owner)) details.Dependencies.Remove(owner);

 

        if (details.Dependencies.Count == 0) Items.Remove(urlOfItem.ToLower());

 

        if (Items.Count == 0)

        {

            _timer.Stop();

            _timer.Dispose();

            _timer = null;

        }

    }

 

    protected void EnsureStarted()

    {

        if (_timer == null)

        {

            _timer = new Timer(10000);

            _timer.AutoReset = true;

            _timer.Elapsed += new ElapsedEventHandler(Timer_Elapsed);

            _timer.Start();

        }

    }

 

    void Timer_Elapsed(object sender, ElapsedEventArgs e)

    {

        SPSite oSite = null;

        try

        {

            oSite = new SPSite(_siteUrl);

 

            string[] arKeys = new string[Items.Keys.Count];

            Items.Keys.CopyTo(arKeys, 0);

 

            for (int c = Items.Count - 1; c >= 0; c--)

            {

                string sKey = arKeys[c];

                try

                {

                    SPFile oFile = oSite.RootWeb.GetFile(sKey);

                    ItemDetails details = Items[sKey];

 

                    if (details.LastModified < oFile.TimeLastModified)

                    {

                        System.Diagnostics.Debug.WriteLine("Raising dependency changed: " + sKey);

                        SPFileCacheDependency[] copy = new SPFileCacheDependency[details.Dependencies.Count];

                        details.Dependencies.CopyTo(copy);

                        foreach (SPFileCacheDependency d in copy)

                            d.RaiseChangedEvent(this, new SPFileCacheEventArgs());

                    }

                }

                catch (Exception ex)

                {

                    System.Diagnostics.Trace.WriteLine(ex);

                }

            }

        }

        catch (Exception ex)

        {

            System.Diagnostics.Trace.WriteLine(ex);

        }

        finally

        {

           oSite.Dispose();

        }

    }

}

public class SPFileCacheEventArgs : EventArgs

{

}

This class does all the work of periodically checking the cached files and invalidating them when they have changed. This implementation currently checks every 10 seconds, this can be changed to suit your own needs.

Now you should be able to cache your SharePoint files and only have to return to SharePoint when the file has actually changed. Personally I use a lot of XSLT and using this technique saves re-loading the XSL which leads to having to re-compile the transform. I cache the compiled XSL object and put a dependency on the XSL file.

You can use this class to monitor any files or even extend it to monitor a list item, a folder or a SharePoint document library.

Posted by Vincent Rothwell on Tuesday, 23 Sep 2008 09:58  - 9 Comments
Orininally printed from http://thekid.me.uk - Copyright Vincent Rothwell 2007
 

Comments

Monday, 6 Oct 2008 11:55 by Tobias Zimmergren
Test comment, had problems commenting to this article yesterday.. Btw, your contact form doesn't work, you'll get an SMTP error :) Cheers

Monday, 6 Oct 2008 02:41 by Vince
Tobias, Not sure what the problem was?? I have just today fixed the SMTP problem...should work now? --Vince

Wednesday, 8 Oct 2008 09:13 by Tudor
Hello, Just to get this right: instead of having the file retrieved on each request by the client's process, you have it retrieved every x seconds (in a separate thread) and notify the subscribers of any change, thus taking the load from the client and adding more stress on the database. In my opinion, an important factor on deciding if you should use this approach or not is the number of requests per second. Alternatively, an improvement on your code would be the Timer_Elapsed method to proceed on opening the SPSite if and only if there are any subscribers registered to handle the event. --Tudor

Thursday, 9 Oct 2008 03:50 by Tudor Olariu
Using polling to implement a caching mechanism is almost always a bad idea. A far better approach would be the use of event handlers which update the cached copy of the object in a database table, and a proxy to provide transparent access to the objects in the database.

Monday, 13 Oct 2008 06:37 by Vince
Tudor, The file is not retrieved, only its metadata...this is much less of an overhead than retrieving the entire file. The response from SQL will be cached by SharePoint and will only be fetched from SQL again if it has changed. The overhead on SQL is minimal. Requests per second are the most important, hence not going to SQL to retrieve a blob, which is very time consuming. A seperate timer thread (which is actually doing anything most of the time) will not affect your requests. Agreed, that would help, but as this is a seperate thread doing that would have only a very slight improvement. Event handlers are not good idea in this situation as they only fire on a single WFE. This is a simple approach to a common problem and works very well for me. I'm sure you can come up with much more elaborate solutions which would perform better, but I'm much more of a KISS person. --Vince

Tuesday, 14 Oct 2008 07:04 by stefan demetz
It might be fun doing it with Velocity - MS Distributed Cache (http://msdn.microsoft.com/en-us/data/cc655792.aspx)

Wednesday, 11 Mar 2009 07:31 by Shawn Cicoria
Did you consider using the GetChanges method on the SPList (or other areas) to retrieve delta situations. The method provides the most efficient and low impact way of getting a changset indication. At the SPList level perhaps the list would contain my cached elements; Via timer and GetChanges, if changes exist, flush the cache, and reset. Just a thought. As for leveraging it accross on Velocity, the GetChanges method also exists on the WS tier, where the SPList class must be accessed ON the WSS box. Certaintly would be an ideal implementation. Nice work.

Sunday, 25 Oct 2009 05:10 by

Thursday, 18 Feb 2010 09:41 by Custom sharepoint cache system
I found a cool sharepoint object caching project on codeplex. http://sharepointcache.codeplex.com It uses delta queries and it's inspied by MOSS cache system.



Url

Email

Comments