Tuesday, May 28, 2013

AppFabric Distributed Caching In SharePoint 2013 Provider Hosted Apps

Chris Domino, Director, Enterprise Architect

One of my favorite new features of SharePoint 2013 is built-in distributed caching, thanks to AppFabric integration. This wasn't available to us out of the box in 2010, which lead my teams down the dark path of custom WCF services deployed to all the front ends on the farm, carefully stitched together via settings in the web.config files.

Pardoning the pun, AppFabric takes care of this stitching for us. When I first dug into the API, I was impressed with how many more options are available to us beyond what the System.Web.Caching.Cache class provides. There are the concepts of regions, Put vs. Add operations, (Put will override an existing key where Add will bomb if the key exists) and all kinds of versioning, locking, and callbacks. These tools let us weave a beautiful, robust quilt of caching that will keep our SharePoint apps warm on even the coldest of performance-degrading nights.

The best part is that we can automatically deploy and configure our cache layer without having to write any code against the 2013 server object model. Avoiding references to Microsoft.SharePoint.dll and its cousins is one of biggest development challenges under the new SharePoint customization paradigms. When I first sat down to learn AppFabric caching (after waiting over twenty seconds for my custom CSOM term-driven navigation to load) I was afraid I was going to have to violate this core principle.

Let me set the stage before jumping into the first Act of the code, which is the deployment bits. Act II will then be the cache layer itself (which, as an inside joke at Rightpoint, I call "CacheMonster"). The project is a public-facing SharePoint 2013 web site, and is built with an S2S provider-hosted (MVC) app whose CSOM-driven controllers provide the data access layer to the SharePoint pages. The site has an online store component, and users are sent to the MVC site directly to consume that content.

I use a ton of PowerShell to drive the deployments, which will be the topic of my next post. Part of this work is sucking in some AppFaric information from SharePoint and writing it to the web.config files. Once this is set, the cache code is ready to rock. Since everything you need is in Microsoft.ApplicationServer.Caching, (and not the SharePoint API) this code is free to run in our MVC site.


On my development machine, I didn't have to do anything to enable or configure AppFabric. My environment is a bare metal Windows Server 2012 box with a domain controller and "complete" SharePoint Enterprise install. When you use the stand alone option, you get a bunch of service applications provisioned for you; complete lands you with a pretty bare bones Central Administration. I mention this detail because even with the bare-bones-ed-ness of my install, I still didn't have to do anything to bring AppFabric to life. So if the following doesn't work for you, make sure AppFabric is installed and activated and whatnot. It also works on Windows Server 2008 R2.


First up, in the aforementioned Act I, is the deployment bits. This performance stars a fun PowerShell script that reads in three values from SharePoint and AppFabric and stuffs them into the web.config of our MVC provider-hosted app. These are the "Cache Host," (the endpoint of the AppFabric cache service) the "Cache Port," (the port of the service) and the "Cache Name (the farm's guid). Here's the script:

Code Listing 1

  1. #initialization
  2. param($webConfigPath = $(Read-Host -prompt "Web.Config File Path"))
  3. #ensure sharepoint
  4. if ((Get-PSSnapin -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) -eq $null)
  5. {
  6. #load snapin
  7. Add-PSSnapIn Microsoft.SharePoint.PowerShell;
  8. }
  9. #open web.config file
  10. $webConfigPath = Join-Path $webConfigPath "web.config";
  11. $xml = [xml] (Get-Content $webConfigPath);
  12. #get cache settings
  13. $cacheHostNode = $xml.SelectSingleNode("/configuration/appSettings/add[@key='CacheHost']");
  14. $cachePortNode = $xml.SelectSingleNode("/configuration/appSettings/add[@key='CachePort']");
  15. $cacheNameNode = $xml.SelectSingleNode("/configuration/appSettings/add[@key='CacheName']");
  16. #get cache info
  17. Use-CacheCluster;
  18. $farm = Get-SPFarm;
  19. $cache = Get-CacheHost;
  20. #ensure cache host
  21. if($cacheHostNode -eq $null)
  22. {
  23. #create cache host node
  24. $element = $xml.CreateElement("add");
  25. $attribute = $xml.CreateAttribute("key");
  26. $attribute.Value = "CacheHost";
  27. $element.Attributes.Append($attribute);
  28. $attribute = $xml.CreateAttribute("value");
  29. $attribute.Value = $cache.HostName.ToString();
  30. $element.Attributes.Append($attribute);
  31. $xml.configuration.appSettings.AppendChild($element);
  32. }
  33. else
  34. {
  35. #update cache host node
  36. $cacheHostNode.Value = $cache.HostName.ToString();
  37. }
  38. #ensure cache port
  39. if($cachePortNode -eq $null)
  40. {
  41. #create cache port node
  42. $element = $xml.CreateElement("add");
  43. $attribute = $xml.CreateAttribute("key");
  44. $attribute.Value = "CachePort";
  45. $element.Attributes.Append($attribute);
  46. $attribute = $xml.CreateAttribute("value");
  47. $attribute.Value = $cache.PortNo.ToString();
  48. $element.Attributes.Append($attribute);
  49. $xml.configuration.appSettings.AppendChild($element);
  50. }
  51. else
  52. {
  53. #update cache port node
  54. $cachePortNode.Value = $cache.PortNo.ToString();
  55. }
  56. #ensure cache name
  57. if($cacheNameNode -eq $null)
  58. {
  59. #create cache name node
  60. $element = $xml.CreateElement("add");
  61. $attribute = $xml.CreateAttribute("key");
  62. $attribute.Value = "CacheName";
  63. $element.Attributes.Append($attribute);
  64. $attribute = $xml.CreateAttribute("value");
  65. $attribute.Value = $farm.Id.ToString();
  66. $element.Attributes.Append($attribute);
  67. $xml.configuration.appSettings.AppendChild($element);
  68. }
  69. else
  70. {
  71. #update cache name node
  72. $cacheNameNode.Value = $farm.Id.ToString();
  73. }
  74. #save
  75. $xml.Save($webConfigPath);

This script needs to be run on every web front end. It takes in the path to the web.config file and gets its content as a blob of XML (Line #11). Next in Line #'s 13-15, we get a reference to the three nodes that represent the app settings for our cache values. Line #'s 17-19 then get objects that represent the SharePoint farm and AppFabric cache service. Here's what the output of these variables looks like in PowerShell ISE:


The rest of the script then takes each value and either creates an AppSettings node for it (the first time the script is run against a particular web.config) or updates the existing setting's value. The PowerShell is a bit verbose here; Line #'s 21-37 show the first of the three blocks that do this XML manipulation work. Notice that whenever I set the value of a node, I call ToString on it (Line #'s 29 and 36 for example). Even if both sides of the assignment are technically strings, PowerShell gets pissy and will passive-aggressively throw an exception to let you know that all of a sudden it cares about types and wants its XML node values to be proper .NET strings.

Cannot set "value" because only strings can be used as values to set XmlNode properties.

Finally, Line #75 saves the web.config file. Once these values are in place, the code in your MVC app can use the AppFabric caching infrastructure. This brings us to Act II, which is my simple little caching layer, a.k.a. the CacheMonster. CacheMonster is a static class that wraps the AppFabric API and provides basic Put/Get operations to the rest of the app. There is a lot more that can be done, (as I mentioned before) but I just wanted to outline the basic operations here.

The core object here is a static instance of Microsoft.ApplicationServer.Caching.DataCache. Following a standard singleton pattern, CacheMonster keeps this object around for the duration of the type's lifetime. Here's the "initialization" code that pulls the web.config values it needs and news up the caching infrastructure.

Code Listing 2

  1. #region Members
  2. private static DataCache _cache;
  3. private static readonly object _lock = new object();
  4. private static DataCache Cache
  5. {
  6. get
  7. {
  8. //lock
  9. lock (CacheMonster._lock)
  10. {
  11. //ensure single instance of cache
  12. if (CacheMonster._cache == null)
  13. {
  14. //configure app fabric
  15. DataCacheFactoryConfiguration config = new DataCacheFactoryConfiguration();
  16. config.Servers = new List<DataCacheServerEndpoint>()
  17. {
  18. //register sharepoint server
  19. new DataCacheServerEndpoint(ConfigurationManager.AppSettings["CacheHost"], Convert.ToInt32(ConfigurationManager.AppSettings["CachePort"]))
  20. };
  21. //get cache
  22. CacheMonster._cache = new DataCacheFactory(config).GetCache(string.Concat(Constants.Cache.Name, new Guid(ConfigurationManager.AppSettings["CacheName"])));
  23. }
  24. //return
  25. return CacheMonster._cache;
  26. }
  27. }
  28. }
  29. #endregion

There's a lot going on here, but it's fairly straight forward: lock to make sure our singleton instance isn't duplicated, build an array of DataCacheServerEndpoint objects, (that will only have one entry: the current front end's AppFabric service) and then pass this configuration information to the DataCacheFactory's GetCac