Implementing CDN based caching for SharePoint internet sites

Why Implement CDN for Internet Sites built on SharePoint

Keeps the content close to end users who are browsing to SharePoint based internet site from a remote location, this can potentially increase the web page performance for end users as browser doesn’t have to go back to the web servers in remote datacenter to get additional files such as css, java script, images etc. that are referenced in the web page. Without CDN caching browser would have to send additional requests back to remote web servers to retrieve those files. Depending on how far the remote users are, this can be a significant performance overhead due to high latency between user’s machine and remote web servers.

In this article I will walk through how you can implement CDN based caching for internet facing sites that are deployed on SharePoint. For the purposes of this article, I’m going to use Windows Azure CDN. This article is not about configuring CDN so I won’t be covering that, how ever for windows Azure CDN you can check out this getting started article which pretty much covers step by step what you need to do https://msdn.microsoft.com/en-us/library/windowsazure/ff919705.aspx

Another key point to keep in mind is that CDN is not something you hook up to all your environments, typically you enable CDN only for your production environment (Authoring/Rendering), so one of the key design goal with this solution was to make it flexible so that you can just activate a feature and all the URLs would now point to CDN resources instead of relative paths pointing to assets stored in SharePoint site.

Files that can be cached in CDN

Typically css files, java script files, images that are used throughout the site that barely changes.

Approach

Custom ASP.NET expressions, that can check if CDN feature is enabled (activated) in Site collection, if the feature is activated, it simply concatenates CDN provider endpoint to the URL. Declarative syntax in HTML will look something like this “<%$CDNUrl:/PublishingImages/SiteLogo.png %>”

Bit of background on ASP.NET Expressions for those who are not much familiar,  simplistically speaking ASP.NET Expressions allows us to declaratively set control properties which will get evaluated at run time when the page is parsed. ASP.NET by default ships with a set of expression builders, for ex. ConnectionStringsExpressionBuildler, AppSettingsExpressionBuildler etc. SharePoint also provides us a set of custom expression builders such as SPUrlExpressionBuilder etc. which knows how to handle “~sitecollection” , “~language” tokens in the expression and replace them when page is parsed.

If you are looking to understand more about ASP.NET expression builders and how to write custom expression builders have a look at this bit old blog post by Chris O’Brien https://www.sharepointnutsandbolts.com/2008/12/using-net-expression-builders-to-set.html

Main components of the solution

Solution contains two features

  1. CDNUrlExpressionBuilder – Activated at web application level. This feature has a feature receiver class wired up that will setup necessary web.config entries required for the custom expression builder when the feature is activated and clears web.config modifications done when the feature is deactivated.
  2. EnableCDNFeature – Activated at Site collection level. This feature also has a feature receiver class wired up that will simply add the CDN provider end point URL to the Property Bag of the Site collection’s root web

Solution also contains two classes

  1. CDNUrlExpressionBuilder – Provides implementation for the CDN URL Expression builder
  2. CDNUrlExpressionEditor – Provides design time editing experience

    

See Code from EvaluateUrlExpression method in CDNUrlExpressionBuilder class which is responsible for handling declarative expressions in HTML, Call to the GetCDNProviderEndPoint method will return CDN provider URL which gets set when you activate the EnableCDNFeature, otherwise it simply returns the relative path to the resource which will point to the asset stored in Site. This will ensure that site continues to render fine in environments where CDN is not enabled.

 public static string EvaluateUrlExpression(string expression)
{
    if (string.IsNullOrEmpty(expression))
        throw new ArgumentNullException(string.Format(Properties.Resources.ParameterCannotBeNull, "expression"));
    string serverRelativeUrlFromPrefixedUrl = SPUtility.GetServerRelativeUrlFromPrefixedUrl(expression);
    for (int i = serverRelativeUrlFromPrefixedUrl.IndexOf("~language", 0, StringComparison.OrdinalIgnoreCase); i > 0; i = serverRelativeUrlFromPrefixedUrl.IndexOf("~language", 0, StringComparison.OrdinalIgnoreCase))
    {
        StringBuilder builder = new StringBuilder(serverRelativeUrlFromPrefixedUrl.Substring(0, i));
        CultureInfo currentUICulture = Thread.CurrentThread.CurrentUICulture;
        builder.Append(currentUICulture.Name);
        if ((i + 9) < serverRelativeUrlFromPrefixedUrl.Length)
        {
            builder.Append(serverRelativeUrlFromPrefixedUrl.Substring(i + 9));
        }
        serverRelativeUrlFromPrefixedUrl = builder.ToString();
    }
    string cdnUrl = GetCDNProviderEndPoint();
    if (!string.IsNullOrEmpty(cdnUrl))
        serverRelativeUrlFromPrefixedUrl = cdnUrl + serverRelativeUrlFromPrefixedUrl;

    return serverRelativeUrlFromPrefixedUrl;
}

        
As you can see below GetCDNProviderEndPoint helper method just simply looks to see if Enable CDN feature is activated and returns the CDNUrl

 

 static string GetCDNProviderEndPoint()
{
    string cdnEndPoint = string.Empty;

    SPSite site = SPContext.Current.Site;
    if (site != null)
    {
        //check if CDN feature is enabled, 
        Guid featureID = new Guid("07dd52dd-898f-426e-b9af-eeb536115041");
        SPFeature enableCDNFeature = site.Features[featureID];
        if (enableCDNFeature != null && site.RootWeb.Properties.ContainsKey("CDNUrl"))
        {
            cdnEndPoint = site.RootWeb.Properties["CDNUrl"];
        }
    }
    return cdnEndPoint;
}

    

CDN expression builder utility is installed for web application using the feature receiver code below

 public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    SPWebService contentService = SPWebService.ContentService;
    contentService.RemoteAdministratorAccessDenied = false;
    contentService.Update();
    
    var webApp = properties.Feature.Parent as SPWebApplication;
    if (webApp != null)
    {
    var configModExprBuilder = new SPWebConfigModification
    {
        Name = "add[@expressionPrefix=\"CDNUrl\"][@type=\"CDNArticle.CDNUrlExpressionBuilder, CDNArticle, Version=1.0.0.0, Culture=neutral, PublicKeyToken=14981b74a0eee3cf\"]",
        Owner = "CDNUrlExpressionBuilder",
        Path = "configuration/system.web/compilation/expressionBuilders",
        Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode,
        Value = "<add expressionPrefix=\"CDNUrl\" type=\"CDNArticle.CDNUrlExpressionBuilder, CDNArticle, Version=1.0.0.0, Culture=neutral, PublicKeyToken=14981b74a0eee3cf\" />"
    };
    webApp.WebConfigModifications.Add(configModExprBuilder);
    var configModAssembly = new SPWebConfigModification
    {
        Name = "add[@assembly=\"CDNArticle, Version=1.0.0.0, Culture=neutral, PublicKeyToken=14981b74a0eee3cf\"]",
        Owner = "CDNUrlExpressionBuilder",
        Path = "configuration/system.web/compilation/assemblies",
        Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode,
        Value = "<add assembly=\"CDNArticle, Version=1.0.0.0, Culture=neutral, PublicKeyToken=14981b74a0eee3cf\" />"
    
    };
    webApp.WebConfigModifications.Add(configModAssembly);
    webApp.Update();
    webApp.Farm.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
}

Feature receiver code below enables CDN for site collection, so the custom expressions in custom master pages and page layouts will now start to reference CDN’d resources instead of relative paths that point to assets stored in site collection

 

 public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
   SPSite site = properties.Feature.Parent as SPSite;
   if (site == null)
       return;

   string cdnUrl = properties.Feature.Properties["CDNUrl"].Value;
   if (site.RootWeb.Properties.ContainsKey("CDNUrl"))
       return;
   //store the CDN Url in property bag for CDN Url expression builder
   SPSecurity.RunWithElevatedPrivileges(() =>
   {
       site.RootWeb.AllowUnsafeUpdates = true;
       site.RootWeb.Properties.Add("CDNUrl", cdnUrl);
       site.RootWeb.Properties.Update();
       site.RootWeb.Update();
       site.RootWeb.AllowUnsafeUpdates = false;
   });
}

 

Just to demonstrate this in action, I threw together a branding sandbox solution which contains a custom master page (starter master pages by MVP Randy Drisgill, if you are involved with branding SharePoint sites you should check it out here https://startermasterpages.codeplex.com/) and deployed to a site created using the out of the box publishing site template.

 

You can see the declarative expression in the screen shot below HTML from the custom master page

 

image

 

I also created storage account in windows azure called “spcache” and CDN enabled it, additionally created three containers “css”, “publishingimages”, “scripts” to store the artifacts that I want to reference from CDN so they are served to end users from a CDN node that is closer to the end user. I used AzureStorageExplorer to manage my storage account in Azure, you can get this tool from here https://azurestorageexplorer.codeplex.com/

 

Screen shot below shows my storage account view using Azure Storage Explorer

 

image 

Screen shots below shows When Enable CDN feature not activated, looking at the view source for the default.aspx you can see the relative URL path reference to styles.css file

image

 

 

image

After activating the CDN enable feature, you can see from the screen shot that shows view source for the default.aspx page that the styles.css is now referencing the CDN’d resource

image

 

image

Hopefully that helps. I’m in the process of uploading this into MSDN code samples site. I will update this thread once the sample is up there in MSDN code samples site.

<Update Date=”3/1/2013”>

Just uploaded the code to MSDN code samples site, go get it from here https://code.msdn.microsoft.com/CDN-Enable-SharePoint-95b73861

</Update>

 

Cheers,

</Ram>