Out of the box, Web API does not support session. It’s possible to enable it, but requires a bit of work. In this blog post, I’ll cover how you can enable session in Web API for your Sitecore solutions with a custom pipeline processor.
Do You Really Need Session?
Before you proceed and enable session for Web API, please read this answer on Stack Overflow about the potential performance impact enabling session could have on your Sitecore site.
Create Extension Method for Session-Enabled Routes
Create an extension method for session-enabled routes:
using System; using System.Net.Http; using System.Web.Http; using System.Web.Http.Routing; namespace WebApiSessionEnabledHandler { public static class HttpRouteCollectionExtensions { public static IHttpRoute MapHttpSessionRoute(this HttpRouteCollection routes, string name, string routeTemplate, bool readOnlySession, object defaults = null, object constraints = null, HttpMessageHandler handler = null) { if (routes == null) throw new ArgumentNullException(nameof(routes)); var dataTokens = new HttpRouteValueDictionary { ["__ReadOnlySession"] = readOnlySession }; var route = routes.CreateRoute(routeTemplate, new HttpRouteValueDictionary(defaults), new HttpRouteValueDictionary(constraints), dataTokens, handler); routes.Add(name, route); return route; } } }
Note the required parameter readOnlySession
that will enable read-only session access for the route when true
, or read-write session access for the route when false
.
Create Session-Enabled HTTP Controller Handlers
Make two HTTP Controller Handlers: one for read-only session, and one for read-write session. Neither of these Controller Handlers do anything but signal to IIS that they need session access, and to what extent.
Read-Only Session HTTP Controller Handler
using System.Web.Http.WebHost; using System.Web.Routing; using System.Web.SessionState; namespace WebApiSessionEnabledHandler { public class ReadOnlySessionHttpControllerHandler : HttpControllerHandler, IReadOnlySessionState { public ReadOnlySessionHttpControllerHandler(RouteData routeData) : base(routeData) { } } }
Read-Write Session HTTP Controller Handler
using System.Web.Http.WebHost; using System.Web.Routing; using System.Web.SessionState; namespace WebApiSessionEnabledHandler { public class SessionRequiredHttpControllerHandler : HttpControllerHandler, IRequiresSessionState { public SessionRequiredHttpControllerHandler(RouteData routeData) : base(routeData) { } } }
Create Session-Enabled HTTP Controller Route Handler
Create a session-enabled HTTP Controller Route Handler that will be responsible for serving one of the Controller Handlers created above for session-enabled routes:
using System; using System.Web; using System.Web.Http.WebHost; using System.Web.Routing; namespace WebApiSessionEnabledHandler { public class SessionEnabledHttpControllerRouteHandler : HttpControllerRouteHandler { protected override IHttpHandler GetHttpHandler(RequestContext requestContext) { var routeData = requestContext.RouteData; if (routeData.DataTokens["__ReadOnlySession"] == null) { throw new InvalidOperationException("This route is not compatible with session."); } var readOnlySession = (bool)routeData.DataTokens["__ReadOnlySession"]; if (readOnlySession) { return new ReadOnlySessionHttpControllerHandler(routeData); } return new SessionRequiredHttpControllerHandler(routeData); } } }
The SessionEnabledHttpControllerRouteHandler
is the brains of this operation; it determines whether the route needs read-write session access or read-only session access and returns the appropriate session-enabled HttpControllerHandler
.
Update Routes to Use Session-Enabled Route Handler
The last piece of the puzzle is to update all registered routes that need session to use the SessionEnabledHttpControllerRouteHandler
. Create a pipeline processor to carry out this task:
using System.Linq; using System.Web.Routing; using Sitecore.Pipelines; namespace WebApiSessionEnabledHandler { public class InitializeSessionEnabledRouteHandlers { public void Process(PipelineArgs args) { var routes = RouteTable.Routes.OfType<Route>().Where(r => r.DataTokens != null); foreach (var route in routes) { if (route.DataTokens["__ReadOnlySession"] != null) { route.RouteHandler = new SessionEnabledHttpControllerRouteHandler(); } } } } }
Create a config
file and put it in a folder prefixed with zzz
. This pipeline processor needs to execute at the end of the initialize
pipeline to ensure it catches all session-enabled routes you register.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <initialize> <processor type="WebApiSessionEnabledHandler.InitializeSessionEnabledRouteHandlers, WebApiSessionEnabledHandler" /> </initialize> </pipelines> </sitecore> </configuration>
Register Routes
Registering session-enabled routes is easy. Instead of using config.Routes.MapHttpRoute
, just use the config.Routes.MapHttpSessionRoute
method.
using System.Web.Http; using Sitecore.Pipelines; namespace WebApiSessionEnabledHandler { public class WebApiConfig { public void Process(PipelineArgs args) { Register(GlobalConfiguration.Configuration); } private static void Register(HttpConfiguration config) { config.Routes.MapHttpSessionRoute( name: "GetMessageApi", routeTemplate: "apisession/getmessage", defaults: new { controller = "GetMessage" }, readOnlySession: true ); config.Routes.MapHttpSessionRoute( name: "SetMessageApi", routeTemplate: "apisession/setmessage", defaults: new { controller = "SetMessage" }, readOnlySession: false ); } } }
Since my GetMessageApi
route will not require write access to the session, I’ve set readOnlySession
to true
.
Plug your routes into the Sitecore pipeline:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <initialize> <processor type="WebApiSessionEnabledHandler.WebApiConfig, WebApiSessionEnabledHandler" patch:after="processor[@type='Sitecore.PathAnalyzer.Services.Pipelines.Initialize.WebApiInitializer, Sitecore.PathAnalyzer.Services']" /> </initialize> </pipelines> </sitecore> </configuration>
Make sure that you always plug in your Web API configuration changes (routes or otherwise) into the initialize
pipeline after Sitecore.PathAnalyzer.Services.Pipelines.Initialize.WebApiInitializer, Sitecore.PathAnalyzer.Services
.
Enjoy Session, Sparingly
Here are the controller
s for my routes:
SetMessageController
public class SetMessageController : ApiController { public IHttpActionResult Post([FromBody]string message) { if (string.IsNullOrEmpty(message)) { return BadRequest(); } HttpContext.Current.Session["SessionMessage"] = message; return Ok(); } }
POST a request to /apisession/setmessage
with message
set in the body to save the message in session.
GetMessageController
public class GetMessageController : ApiController { public IHttpActionResult Get() { var session = HttpContext.Current.Session; var sessionMessage = (string)session["SessionMessage"]; if (string.IsNullOrEmpty(sessionMessage)) { return NotFound(); } return Ok($"Retrieved message from Session: {sessionMessage}"); } }
Navigate to /apisession/getmessage
to get the message from session.
Enable Analytics (Proof of Concept)
If you’ve played with Web API in your Sitecore projects at all, you may have tried to enable Analytics with Tracker.StartTracking()
and run into the following exception:
An exception of type ‘System.InvalidOperationException’ occurred in Sitecore.Analytics.dll but was not handled in user code
Additional information: Tracker.Current is not initialized
This is due to session not being available. With the ability to turn on session comes the ability to enable Analytics. For this proof of concept, I will expose an endpoint that provides the IP address used for Analytics using built-in Sitecore functionality.
Create HttpMessageHandler to Enable Analytics and Get Forwarded IP
HttpMessageHandlers
execute before and after your controller in a pipeline fashion not unlike Sitecore’s pipelines. Microsoft has an awesome poster that outlines how they work.
using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Sitecore.Analytics; namespace WebApiSessionEnabledHandler { public class EnableTrackingHandler : DelegatingHandler { protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { Tracker.StartTracking(); var response = base.SendAsync(request, cancellationToken); Tracker.Current.EndTracking(); return response; } } }
This handler enables tracking before your route’s controller
(s) execute, and stops tracking after they have finished executing.
Plug HttpMessageHandler into Session-Enabled Route
Use HttpClientFactory.CreatePipeline
to create a new HttpControllerDispatcher
with the EnableTrackingHandler
plugged in. Add the pipeline into your session-enabled routes as the handler
parameter.
using System.Net.Http; using System.Web.Http; using System.Web.Http.Dispatcher; using Sitecore.Pipelines; namespace WebApiSessionEnabledHandler { public class WebApiConfig { public void Process(PipelineArgs args) { Register(GlobalConfiguration.Configuration); } public static void Register(HttpConfiguration config) { var analyticsRouteHandlers = HttpClientFactory.CreatePipeline( new HttpControllerDispatcher(config), new[] { new EnableTrackingHandler() } ); config.Routes.MapHttpSessionRoute( name: "AnalyticsIp", routeTemplate: "apisession/analyticsip", defaults: new { controller = "AnalyticsIp" }, readOnlySession: false, constraints: null, handler: analyticsRouteHandlers ); } } }
Return Forwarded IP Address from Controller
With Tracking enabled, I can call Tracker.Current
and access analytics data in my controller
s. I’ve added a patch for Analytics to pull IP addresses from the X-Forwarded-For
header–this allows me to pass in a test IP address from Postman when developing locally (otherwise, my IP address will always resolve as 127.0.0.1
with no Geo IP data, etc.).
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <settings> <setting name="Analytics.ForwardedRequestHttpHeader"> <patch:attribute name="value">X-Forwarded-For</patch:attribute> </setting> </settings> </sitecore> </configuration>
And my controller
code:
public class AnalyticsIpController : ApiController { public IHttpActionResult Get() { if (Tracker.Current?.Interaction == null) { return NotFound(); } var forwardedIp = new IPAddress(Tracker.Current.Interaction.Ip); return Ok(forwardedIp.ToString()); } }
Now calls to /apisession/analyticsip
with the X-Forwarded-For
header set will return the IP address.
Conclusion
This isn’t the only approach to enabling session in Web API 2. Another approach is to create an HTTP Module that inspects every request and enables session based on route data, similar to what I’ve demonstrated here. I’ve got a working example on GitHub if interested. The HTTP Module approach is nice because you can get away without any kind of Sitecore configuration, but it adds overhead to every single request that comes into your site–a bit too much in my opinion. The approach I’ve outlined above affects only requests to routes you register as needing session, which is more focused and quite cleaner.
Let me know your thoughts in the comments.
Idea/execution credit: http://stackoverflow.com/a/13758602/1646962 (warrickh)
Source code on GitHub: https://github.com/coreyasmith/WebApiEnableSessionHandler
Great info and nice article! Definately will help in my Sitecore journey.
Glad it helped! Thanks for reading.