Making SharePoint Apps Scale with Azure Redis Cache
This post will show how to create custom classes for a SharePoint 2013 app that enable the use of Azure Redis Cache.
Background
Often times there are web application design constraints that require use of session state. From Scott Guthrie’s book:
It's often not practical in a real-world cloud app to avoid storing some form of state for a user session, but some approaches impact performance and scalability more than others. If you have to store state, the best solution is to keep the amount of state small and store it in cookies. If that isn't feasible, the next best solution is to use ASP.NET session state with a provider for distributed, in-memory cache. (See “ASP.NET session state using a cache provider,” in Chapter 12, “Distributed Caching.”) The worst solution from a performance and scalability standpoint is to use a database-backed session state provider.
[page 56 of Building Cloud Apps with Microsoft Azure: Best practices for DevOps, data storage, high availability, and more , by Scott Guthrie, Mark Simms, Tom Dykstra, Rick Anderson, and Mike Wasson]
Azure Websites make a very compelling target for provider-hosted apps that target Office 365. The default code generated by Visual Studio for SharePoint apps using the SharePointContext class require ASP.NET session state, and the default is to use InProc mode for session state. Of course that’s not going to work when we scale our website to more than 1 instance. To achieve scale, I thought I would use Azure Redis Cache to store session state data (instead of a database as per the above quote). I followed the instructions here and then received this error:
Type 'KirkeDemoWeb.SharePointContext' in Assembly 'KirkeDemoWeb, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.
OK, fair enough, I’ll just mark is with the SerializableAttribute. Then I hit this error:
Type 'Microsoft.IdentityModel.S2S.Tokens.JsonWebSecurityToken' in Assembly 'Microsoft.IdentityModel.Extensions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=69c3241e6f0468ca' is not marked as serializable.
Since I don’t have the source code to that class, I can’t mark it as serializable. The rest of this post shows how to solve the problem by creating a new SharePointContext implementation that is serializable.
The Problem
When you generate a new App for SharePoint project in Visual Studio, the code will store sensitive information in session data, namely the OAuth refresh and access tokens that should not be stored on the client. For this reason, the SharePoint context token provides a CacheKey claim in the context token that is used to safely store in an HTTP cookie while the corresponding data can be stored in session state, retrieved by using the CacheKey. The out-of-box code generated by Visual Studio uses a provider model to save the refresh and access tokens to session state, reducing the number of round trips to obtain an OAuth token.
The use of session state here is appropriate, but as we saw before we hit the error that SharePointContextToken is not marked serializable. The reason why is highlighted in the class diagram.
The solution is to create a new SharePointContext implementation that does not have the contextTokenObj (of type SharePointContextToken) field. If we go into the source and find all references to the contextTokenObj field, we can get a better sense of how it is being used:
The lines highlighted in the green box are simply checking the ValidTo property to determine when the context token is no longer valid. That’s easy enough to replace. The code in the red box obtains an access token. It turns out that TokenHelper makes replacing this code easy as well.
SharePointAcsSerializableContext
The first part of the solution is to create a new class that derives from SharePointContext. Instead of using the SharePointContextToken class, we simply capture the required data in fields within our class. You can see the difference in this class diagram, where we take the data that was stored in the SharePointContextToken class and store it within fields in our class. All of the fields are native .NET types, meaning they are all serializable.
The implementation is provided in full.
Serializable SharePointContext
- [Serializable]
- publicclassSharePointAcsSerializableContext : SharePointContext
- {
- privatereadonlystring _contextToken;
- privateDateTime? _contextTokenValidTo;
- privatestring _refreshToken;
- privatestring _targetPrincipalName;
- privatestring _targetRealm;
- privatestring _cacheKey;
- privateDateTime ContextTokenValidTo
- {
- get
- {
- returnthis._contextTokenValidTo == null ? DateTime.MinValue : this._contextTokenValidTo.Value;
- }
- }
- ///<summary>
- /// The context token.
- ///</summary>
- publicstring ContextToken
- {
- get { return ContextTokenValidTo > DateTime.UtcNow ? this._contextToken : null; }
- }
- ///<summary>
- /// The context token's "CacheKey" claim.
- ///</summary>
- publicstring CacheKey
- {
- get { return ContextTokenValidTo > DateTime.UtcNow ? this._cacheKey : null; }
- }
- ///<summary>
- /// The context token's "refreshtoken" claim.
- ///</summary>
- publicstring RefreshToken
- {
- get { return ContextTokenValidTo > DateTime.UtcNow ? this._refreshToken : null; }
- }
- publicoverridestring UserAccessTokenForSPHost
- {
- get
- {
- return GetAccessTokenString(refthis.userAccessTokenForSPHost,
- () => TokenHelper.GetAccessToken(this._refreshToken, this._targetPrincipalName, this.SPHostUrl.Authority, this._targetRealm));
- }
- }
- publicoverridestring UserAccessTokenForSPAppWeb
- {
- get
- {
- if (this.SPAppWebUrl == null)
- {
- returnnull;
- }
- return GetAccessTokenString(refthis.userAccessTokenForSPAppWeb,
- () => TokenHelper.GetAccessToken(this._refreshToken, this._targetPrincipalName, this.SPAppWebUrl.Authority, this._targetRealm));
- }
- }
- publicoverridestring AppOnlyAccessTokenForSPHost
- {
- get
- {
- return GetAccessTokenString(refthis.appOnlyAccessTokenForSPHost,
- () => TokenHelper.GetAppOnlyAccessToken(TokenHelper.SharePointPrincipal, this.SPHostUrl.Authority, TokenHelper.GetRealmFromTargetUrl(this.SPHostUrl)));
- }
- }
- publicoverridestring AppOnlyAccessTokenForSPAppWeb
- {
- get
- {
- if (this.SPAppWebUrl == null)
- {
- returnnull;
- }
- return GetAccessTokenString(refthis.appOnlyAccessTokenForSPAppWeb,
- () => TokenHelper.GetAppOnlyAccessToken(TokenHelper.SharePointPrincipal, this.SPAppWebUrl.Authority, TokenHelper.GetRealmFromTargetUrl(this.SPAppWebUrl)));
- }
- }
- public SharePointAcsSerializableContext(Uri spHostUrl, Uri spAppWebUrl, string spLanguage, string spClientTag, string spProductNumber, string contextToken, SharePointContextToken contextTokenObj)
- : base(spHostUrl, spAppWebUrl, spLanguage, spClientTag, spProductNumber)
- {
- if (string.IsNullOrEmpty(contextToken))
- {
- thrownewArgumentNullException("contextToken");
- }
- if (contextTokenObj == null)
- {
- thrownewArgumentNullException("contextTokenObj");
- }
- this._contextToken = contextToken;
- this._refreshToken = contextTokenObj.RefreshToken;
- this._targetPrincipalName = contextTokenObj.TargetPrincipalName;
- this._targetRealm = contextTokenObj.Realm;
- this._contextTokenValidTo = contextTokenObj.ValidTo;
- this._cacheKey = contextTokenObj.CacheKey;
- }
- ///<summary>
- /// Ensures the access token is valid and returns it.
- ///</summary>
- ///<param name="accessToken">The access token to verify.</param>
- ///<param name="tokenRenewalHandler">The token renewal handler.</param>
- ///<returns>The access token string.</returns>
- privatestaticstring GetAccessTokenString(refTuple<string, DateTime> accessToken, Func<OAuth2AccessTokenResponse> tokenRenewalHandler)
- {
- RenewAccessTokenIfNeeded(ref accessToken, tokenRenewalHandler);
- return IsAccessTokenValid(accessToken) ? accessToken.Item1 : null;
- }
- ///<summary>
- /// Renews the access token if it is not valid.
- ///</summary>
- ///<param name="accessToken">The access token to renew.</param>
- ///<param name="tokenRenewalHandler">The token renewal handler.</param>
- privatestaticvoid RenewAccessTokenIfNeeded(refTuple<string, DateTime> accessToken, Func<OAuth2AccessTokenResponse> tokenRenewalHandler)
- {
- if (IsAccessTokenValid(accessToken))
- {
- return;
- }
- try
- {
- OAuth2AccessTokenResponse oAuth2AccessTokenResponse = tokenRenewalHandler();
- DateTime expiresOn = oAuth2AccessTokenResponse.ExpiresOn;
- if ((expiresOn - oAuth2AccessTokenResponse.NotBefore) > AccessTokenLifetimeTolerance)
- {
- // Make the access token get renewed a bit earlier than the time when it expires
- // so that the calls to SharePoint with it will have enough time to complete successfully.
- expiresOn -= AccessTokenLifetimeTolerance;
- }
- accessToken = Tuple.Create(oAuth2AccessTokenResponse.AccessToken, expiresOn);
- }
- catch (WebException)
- {
- }
- }
- }
For the most part, this is the same implementation as the out of box SharePointAcsContext class. Besides the changed logic for checking the context token’s expiration date (lines 13-43) and setting the field properties (lines 104-110), it’s the same code. One additional thing to call out is that we changed the logic to obtain a new access token (lines 50 and 64) to use the individual parameters instead of passing a SharePointContextToken object.
SharePointAcsSerializableContextProvider
Now that we’ve created the new class, how do we use it? As mentioned previously, SharePointContext uses a provider pattern. The provider class is responsible for creating the appropriate SharePointContext derived type. Our new class is shown in the diagram.
The implementation here is identical to the SharePointAcsContextProvider class, we just replace any references to SharePointAcsContext with SharePointAcsSerializableContext.
Provider
- publicclassSharePointAcsSerializableContextProvider : SharePointContextProvider
- {
- privateconststring SPContextKey = "SPContext";
- privateconststring SPCacheKeyKey = "SPCacheKey";
- protectedoverrideSharePointContext CreateSharePointContext(Uri spHostUrl, Uri spAppWebUrl, string spLanguage, string spClientTag, string spProductNumber, HttpRequestBase httpRequest)
- {
- string contextTokenString = TokenHelper.GetContextTokenFromRequest(httpRequest);
- if (string.IsNullOrEmpty(contextTokenString))
- {
- returnnull;
- }
- SharePointContextToken contextToken = null;
- try
- {
- contextToken = TokenHelper.ReadAndValidateContextToken(contextTokenString, httpRequest.Url.Authority);
- }
- catch (WebException)
- {
- returnnull;
- }
- catch (AudienceUriValidationFailedException)
- {
- returnnull;
- }
- returnnewSharePointAcsSerializableContext(spHostUrl, spAppWebUrl, spLanguage, spClientTag, spProductNumber, contextTokenString, contextToken);
- }
- protectedoverridebool ValidateSharePointContext(SharePointContext spContext, HttpContextBase httpContext)
- {
- SharePointAcsSerializableContext spAcsContext = spContext asSharePointAcsSerializableContext;
- if (spAcsContext != null)
- {
- Uri spHostUrl = SharePointContext.GetSPHostUrl(httpContext.Request);
- string contextToken = TokenHelper.GetContextTokenFromRequest(httpContext.Request);
- HttpCookie spCacheKeyCookie = httpContext.Request.Cookies[SPCacheKeyKey];
- string spCacheKey = spCacheKeyCookie != null ? spCacheKeyCookie.Value : null;
- return spHostUrl == spAcsContext.SPHostUrl &&
- !string.IsNullOrEmpty(spAcsContext.CacheKey) &&
- spCacheKey == spAcsContext.CacheKey &&
- !string.IsNullOrEmpty(spAcsContext.ContextToken) &&
- (string.IsNullOrEmpty(contextToken) || contextToken == spAcsContext.ContextToken);
- }
- returnfalse;
- }
- protectedoverrideSharePointContext LoadSharePointContext(HttpContextBase httpContext)
- {
- return httpContext.Session[SPContextKey] asSharePointAcsSerializableContext;
- }
- protectedoverridevoid SaveSharePointContext(SharePointContext spContext, HttpContextBase httpContext)
- {
- SharePointAcsSerializableContext spAcsContext = spContext asSharePointAcsSerializableContext;
- if (spAcsContext != null)
- {
- HttpCookie spCacheKeyCookie = newHttpCookie(SPCacheKeyKey)
- {
- Value = spAcsContext.CacheKey,
- Secure = true,
- HttpOnly = true
- };
- httpContext.Response.AppendCookie(spCacheKeyCookie);
- }
- httpContext.Session[SPContextKey] = spAcsContext;
- }
- }
Registering the Provider
Now that we have a context implementation and provider, we need to register the provider for our web application. We do that in Global.asax.cs. Rather than add the code there, we follow the pattern for other customizations by creating a new class, SharePointConfig.cs located in the App_Start folder of our web application.
SharePointConfig
- publicclassSharePointConfig
- {
- publicstaticvoid RegisterProvider()
- {
- //Register the serializable context provider as the current
- SharePointContextProvider.Register(newSharePointAcsSerializableContextProvider());
- }
- }
Now in Global.asax.cs in Application_Start, we reference our new class.
Application_Start
- protectedvoid Application_Start()
- {
- AreaRegistration.RegisterAllAreas();
- IdentityConfig.ConfigureIdentity();
- FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
- RouteConfig.RegisterRoutes(RouteTable.Routes);
- BundleConfig.RegisterBundles(BundleTable.Bundles);
- SharePointConfig.RegisterProvider();
- }
When the application starts, our provider is registered as the current provider. That’s it, the rest of our code is untouched.
The Payoff
The payoff is that we don’t have to make any changes to our implementation code. We can write the same code that can easily be used with multiple providers just by switching configuration. Notice that we do not directly reference the SharePointAcsSerializableContext because the provider model handles this for us.
Code Snippet
- publicvoid foo()
- {
- SharePointContext spContext = SharePointContextProvider.Current.GetSharePointContext(HttpContext.Current);
- string listUrl = "";
- using (var clientContext = spContext.CreateUserClientContextForSPHost())
- {
- if (clientContext != null)
- {
- //Do something with context here
- }
- }
- }
Very cool, the implementation details are abstracted away from our code.
Using Azure Redis Cache
Because the types are serializable, we can now use Azure Redis Cache as a custom session state provider. First, create a new Azure Redis Cache by going to https://portal.azure.com.
Create a new Azure Redis Cache (this process takes around 30 minutes).
Once created, go to the cache and obtain the key.
Add the NuGet package for RedisSessionStateProvider to your web application.
Change the system.web/sessionState configuration section as shown, using your own cache host and key.
Web.config
- <sessionStatemode="Custom"customProvider="CacheProvider">
- <providers>
- <add
- name="CacheProvider"
- type="Microsoft.Web.Redis.RedisSessionStateProvider"
- host="kirkedemo.redis.cache.windows.net"
- accessKey="asdfhgdrthtrsawefdaserhy7jsergwaeg="
- ssl="true" />
- </providers>
- </sessionState>
You have now moved your session state to a scalable, distributed, secure cache, allowing your web site to scale to many instances.
Summary
This post showed how you how to use Azure Redis Cache for session state and how to create a SharePointContext provider that can be serialized using Azure Redis Cache.
For More Information
MVC Movie App with Azure Redis Cache in 15 Minutes
Comments
Anonymous
October 02, 2014
SharePoint 2013 uses AppFabric as a distributed cache, with admittedly "interesting" results, although not for session state management. It would be interesting to find a way to replace AppFabric with Redis in these scenarios as well. Having multiple caching products on the same farm isn't ideal.Anonymous
October 02, 2014
You don't have multiple caching products on the same farm. AppFabric remains a dependency for SharePoint 2013, while Azure Redis Cache is an Azure service that is a dependency for my app. The app is completely separate from SharePoint, apps and SharePoint share zero infrastructure. My app continues to live wherever I deem the best fit, and it communicates remotely to your SharePoint farm.Anonymous
October 02, 2014
Hi Kirk, thank for sharing! Interesting info.Anonymous
October 15, 2014
Kirk, why when I click a button, the key is in the cookie or the session is not recreated after 20 min of inactivity? Know what can be?Anonymous
October 16, 2014
A big thank you to Kirk! This saved us a lot or time in overcoming the issue. Hope Microsoft will update their built in code in visual studio 2013.Anonymous
October 16, 2014
A big THANK YOU to Kirk! This saved us a lot of time! Hope Microsoft will update visual studio 2013...Anonymous
October 19, 2014
Kirk, Thanks for the post. I think that an interesting side effect of this is that you use a SharePoint context in a custom REST api of your provider hosted app. I did something similar to this using the .Net memory cache in order to re-hydrate a acs context from a context token and that spcachekey cookie, allowing me to get a context in a 'stateless' call. It works well, but it's not quite as elegant as what you have here and it won't scale as well either. I'll definitely have to try this out and see how it feels. Thanks again.Anonymous
November 25, 2014
Very interesting post, sure I will use it soon.Anonymous
November 25, 2014
Kirk, do you know whats the difference between StackExchange.Redis (azure.microsoft.com/.../cache-dotnet-how-to-use-azure-redis-cache) and RedisStateSessionProvider packages?Anonymous
January 12, 2015
I have problems to implement. The SharePointContext class returns an error because it can not be serialized. I followed step by step, but I can not resolve the error.Anonymous
January 13, 2015
@Rafael - make sure to edit the SharePointContext class and add the [Serializable] attribute to the class.Anonymous
August 28, 2015
The comment has been removedAnonymous
August 28, 2015
Please ignore I have it building now thanks!Anonymous
August 31, 2015
@Laurence - thanks for pointing this out. Yes, you have to mark the SharePointContext class serializable. I mention this in the beginning, but didn't do a good job highlighting how to do this. Thanks for pointing this out for others that may have the same issue.Anonymous
December 08, 2015
Wouldn't using node affinity on a load balancer address the issue of scaling out to more than 1 instance? That way we don't have to use external caching infrastructure and rewrite the SharePointContextToken class?