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.