Monday, July 19, 2010

SharePoint Cache Callback

Using ASP.NET Cache Callback, we can update the data automatically in background. This cache update callback mechanism can also be applied to SharePoint environment.

For example, SPList.Items.GetDataTable() is expensive and we may need to cache the returned datatable in memory; when the cache is expired, in the cache callback function we check if the cache data is updated or not, and refresh the cache data if corresponding data is changed in the content database.

However there's a pitfall of this approach: the cache is first inserted with user identity, but the callback functions are run with application pool identity. They may have different permissions and have different data set.

Take anonymous access as an example. Caching is usually enabled for high-volume public sites with anonymous access. Anonymous users have very limited permissions comparing to the app-pool account (system account). To resolve the problem in such case, we need to impersonate anonymous user in update callback. In SharePoint this is done by opening a site with anonymous SPUserToken. In order to get a SPUserToken with anonymous privilege, we can assign a domain service account with anonymous permissions, and get this pre-defined SPUser at run time. Code example:
    // Cache update callback function
private static void SPList_UpdateCallBack(
string key,
CacheItemUpdateReason reason,
out object value,
out CacheDependency dependency,
out DateTime expirationDateTime,
out TimeSpan slidingExpiration)
{
string siteUrl = null;
string listName = null;
DataTable newDT = null;
DataTable oldDT = HttpRuntime.Cache[key] as DataTable;
value = oldDT;

if (reason == CacheItemUpdateReason.Expired)
{
// Check if data has not been changed in database
// Only update the cache when there's change

SPSite anonymousSite = null;
try
{
string errMessage = string.Empty;
SPUserToken userToken = SPSecurityHelper.GetAnonymousUserToken(siteUrl, out errMessage);
if (!string.IsNullOrEmpty(errMessage) || userToken == null)
{
// Log error
}

// Create a SPSite with anonymous user token
anonymousSite = new SPSite(siteUrl, userToken);
if (anonymousSite != null)
{
using (SPWeb anonymousweb = anonymousSite.RootWeb)
{
SPList anonylist = anonymousweb.Lists[listName];
newDT = anonylist.Items.GetDataTable();
}
anonymousSite.Dispose();
}
value = newDT;
}
catch (Exception ex)
{
//Log error
if (oldDT != null)
value = oldDT.Copy();
else
value = null;
}
}

dependency = null;
expirationDateTime = DateTime.Now.AddSeconds(300);
slidingExpiration = Cache.NoSlidingExpiration;
}

// Helper class to get anonymous token
public class SPSecurityHelper
{
private static object lockObj = new object();
private static SPUserToken AnonymousToken = null;
public static SPUserToken GetAnonymousUserToken(string siteURL, out string errorMessage)
{
string msg = string.Empty;
if (AnonymousToken == null)
{
lock (lockObj)
{
if (AnonymousToken == null)
{
try
{
SPSecurity.RunWithElevatedPrivileges(delegate()
{
using (SPSite site = new SPSite(siteURL))
{
using (SPWeb web = site.RootWeb)
{
string userLoginName = System.Configuration.ConfigurationSettings.AppSettings["AnonymousUser.LoginName"];
if (string.IsNullOrEmpty(userLoginName))
{
msg = "Failed in getting anonymous user token: missing AnonymousUser.LoginName in Web.Config.";
}
else
{
SPUserCollection users = web.Users.GetCollection(new string[] { userLoginName });
if (users != null && users.Count == 1 && users[0].UserToken != null)
{
AnonymousToken = users[0].UserToken;
}
else
{
msg = string.Format("Failed to get user token of {0} from SharePoint.", userLoginName);
}
}
}
}
});
}
catch (Exception ex)
{
msg = ex.Message;
throw;
}
}
}
}

errorMessage = msg;
return AnonymousToken;
}
}