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 controllers 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 controllers. 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/sitecore-web-api-session