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 http://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 http://www.sharepointnutsandbolts.com/2008/12/using-net-expression-builders-to-set.html
Main components of the solution
Solution contains two features
Solution also contains two classes
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;}
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();}
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 http://startermasterpages.codeplex.com/) and deployed to a site created using the out of the box publishing site template.
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
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
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 http://code.msdn.microsoft.com/CDN-Enable-SharePoint-95b73861
</Update>
Cheers,
</Ram>