Web API Attribute Routing in Sitecore 7.5 and later (up to 8.1 Update-3)

Web API Attribute Routing in Sitecore 7.5 and later (up to 8.1 Update-3)

UPDATE 18/10/2016: This has now been fixed in Sitecore 8.2 and Attribute Routing works out of the box. Sitecore.Services.Client takes care of mapping the attributes during the initialize pipeline so you shouldn’t do that yourself or it will break.

Read about the changes to Attribute Routing in 8.2 here.

Starting with Sitecore 7.5 they added some new web services to the product, which conflicts with how Attribute Routing works. So if you can’t get Web API Attribute Routing to work with your clean install of Sitecore 7.5, 8.0 or 8.1 or you have upgraded from an earlier version and your routes no longer works – then this is the reason.

If you just try to use Attribute Routing like normal you will get a response looking something like this:

Message: "An error has occurred.",
ExceptionMessage: "ValueFactory attempted to access the Value property of this instance.",
ExceptionType: "System.InvalidOperationException",
StackTrace: "
at System.Lazy`1.CreateValue() 
at System.Lazy`1.LazyInitValue() 
at System.Web.Http.Dispatcher.DefaultHttpControllerSelector.GetControllerMapping() 
at System.Web.Http.Routing.AttributeRoutingMapper.AddRouteEntries(SubRouteCollection collector, HttpConfiguration configuration, IInlineConstraintResolver constraintResolver, IDirectRouteProvider directRouteProvider) 
at System.Web.Http.Routing.AttributeRoutingMapper.<>c__DisplayClass2.<>c__DisplayClass4.<MapAttributeRoutes>b__1() 
at System.Web.Http.Routing.RouteCollectionRoute.EnsureInitialized(Func`1 initializer) 
at System.Web.Http.Routing.AttributeRoutingMapper.<>c__DisplayClass2.<MapAttributeRoutes>b__0(HttpConfiguration config) 
at System.Web.Http.HttpConfiguration.ApplyControllerSettings(HttpControllerSettings settings, HttpConfiguration configuration) 
at System.Web.Http.Controllers.HttpControllerDescriptor.InvokeAttributesOnControllerType(HttpControllerDescriptor controllerDescriptor, Type type) 
at System.Web.Http.Controllers.HttpControllerDescriptor..ctor(HttpConfiguration configuration, String controllerName, Type controllerType) 
at System.Web.Http.Dispatcher.DefaultHttpControllerSelector.InitializeControllerInfoCache() 
at System.Lazy`1.CreateValue() 
at System.Lazy`1.LazyInitValue() 
at System.Web.Http.Dispatcher.DefaultHttpControllerSelector.GetControllerMapping() 
at System.Web.Http.Routing.AttributeRoutingMapper.AddRouteEntries(SubRouteCollection collector, HttpConfiguration configuration, IInlineConstraintResolver constraintResolver, IDirectRouteProvider directRouteProvider) 
at System.Web.Http.Routing.AttributeRoutingMapper.<>c__DisplayClass2.<>c__DisplayClass4.<MapAttributeRoutes>b__1() 
at System.Web.Http.Routing.RouteCollectionRoute.EnsureInitialized(Func`1 initializer) 
at System.Web.Http.Routing.AttributeRoutingMapper.<>c__DisplayClass2.<MapAttributeRoutes>b__0(HttpConfiguration config) 
at System.Web.Http.HttpConfiguration.ApplyControllerSettings(HttpControllerSettings settings, HttpConfiguration configuration) 
at System.Web.Http.Controllers.HttpControllerDescriptor.InvokeAttributesOnControllerType(HttpControllerDescriptor controllerDescriptor, Type type) 
at System.Web.Http.Controllers.HttpControllerDescriptor..ctor(HttpConfiguration configuration, String controllerName, Type controllerType) 
at Sitecore.Services.Infrastructure.Web.Http.Dispatcher.NamespaceHttpControllerSelector.InitializeControllerDictionary() 
at System.Lazy`1.CreateValue() 
at System.Lazy`1.LazyInitValue() 
at Sitecore.Services.Infrastructure.Web.Http.Dispatcher.NamespaceHttpControllerSelector.FindMatchingController(String namespaceName, String controllerName) 
at Sitecore.Services.Infrastructure.Web.Http.Dispatcher.NamespaceHttpControllerSelector.SelectController(HttpRequestMessage request) 
at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()"

I actually created a NuGet package for this a few months ago, and the readme on the GitHub project page explains the problem and solution pretty well, but I thought I would just quickly go through it here on my blog as well. I got most of my solution from this blog post by Bart Bovendeerdt.

There is also a “Custom” version of the NuGet package which adds the source code directly to your solution, so you can modify it to your needs. I also made a Sitecore package for Sitecore Marketplace if you prefer that.

My solution shouldn’t interfere with any of Sitecore’s own stuff (not that I have noticed anyway) – it just adds support for Attribute Routing.

The issue

With the Sitecore.Services.Client.config include file, Sitecore replaces the DefaultHttpControllerSelector with their own NamespaceHttpControllerSelector which doesn’t work with Attribute Routing. To fix the problem we just need to extend the NamespaceHttpControllerSelector and add a little bit of code in the SelectController() method, but because of private fields/methods in the class we need to copy it all.

// Decompiled (and cleaned) code from NamespaceHttpControllerSelector
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
    var routeData = request.GetRouteData();
    var namespaceVariable = GetRouteVariable<string>(routeData, NamespaceKey);
    var controllerVariable = GetRouteVariable<string>(routeData, ControllerKey);

    var httpControllerDescriptor = FindMatchingController(namespaceVariable, controllerVariable);
    if (httpControllerDescriptor != null)
    {
        return httpControllerDescriptor;
    }

    throw new HttpResponseException(HttpStatusCode.NotFound);
}

The solution

As mentioned, the solution is pretty simple, we just need to add two lines of code to this method.

// Decompiled (and cleaned) code from NamespaceHttpControllerSelector
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
    var routeData = request.GetRouteData();
    var namespaceVariable = GetRouteVariable<string>(routeData, NamespaceKey);
    var controllerVariable = GetRouteVariable<string>(routeData, ControllerKey);

    // FIX
    // Use default logic if no controller variable exist
    if (string.IsNullOrEmpty(controllerVariable))
        return base.SelectController(request);

    var httpControllerDescriptor = FindMatchingController(namespaceVariable, controllerVariable);
    if (httpControllerDescriptor != null)
    {
        return httpControllerDescriptor;
    }

    throw new HttpResponseException(HttpStatusCode.NotFound);
}

We simply check if the **controllerVariable **is empty and fall back to the default implementation of DefaultHttpControllerSelector if it is.

Now we just need to replace the IHttpControllerSelector with our new implementation and setup Attribute Routing as usual. You do this in several ways, using WebActivator or Global.asax.cs, but I prefer to do this in the initialize pipeline of Sitecore.

public class ConfigureAttributeRouting
{
    public void Process(PipelineArgs args)
    {
        // Setup Attribute Routing
        GlobalConfiguration.Configure(config => config.MapHttpAttributeRoutes());

        // Replace the IHttpControllerSelector with our own implementation
        GlobalConfiguration.Configure(ReplaceControllerSelector);
    }

    private static void ReplaceControllerSelector(HttpConfiguration config)
    {
        config.Services.Replace(typeof (IHttpControllerSelector),
            new CustomHttpControllerSelector(config, new NamespaceQualifiedUniqueNameGenerator()));
    }
}

I’ve found that the above has to be done in that order for it to work correctly. I don’t have a good enough understanding of the internals of this to tell you why.

We also need to create an include file to actually run the above in the initialize pipeline.

Note: Make sure this file is placed in a subfolder of App_Config/Include or name it something, so it sorts after Sitecore.Services.Client.config (as the include files are processed in alphabetic order).

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="YourAssembly.ConfigureAttributeRouting, YourAssembly"
                   patch:after="processor[@type='Sitecore.Services.Infrastructure.Sitecore.Pipelines.ServicesWebApiInitializer, Sitecore.Services.Infrastructure.Sitecore']" />
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

We need to have our processor run after Sitecore’s, otherwise their processor will just replace our implementation of the IHttpControllerSelector.

Extra: Camel casing JSON response

When working with JSON you usually want to leverage the CamelCasePropertyNamesContractResolver which turns your PascalCased C# models into camelCased JSON which better suits the JavaScript conventions. Normally you would do that when also configuring Attribute Routing, like this:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();
        config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
    }
}

But for some reason, which I haven’t figured out yet, this doesn’t work. I suppose Sitecore is configuring this in one of their processors somewhere and overwriting our configuration. Anyway, we might not want to do it for all JSON responses as it might break some of Sitecore’s code.

The solution I’ve used is to create a CamelCaseJson attribute that I put on my API controllers. I can then easily choose which controllers should use camel casing and at the same time I’m sure not to mess with anything of Sitecore’s stuff.

public class CamelCaseJsonAttribute : Attribute, IControllerConfiguration
{
    public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor)
    {
        controllerSettings.Formatters.Clear();
        controllerSettings.Formatters.Add(new JsonMediaTypeFormatter());
        controllerSettings.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
    }
}

I then just use it like this:

[CamelCaseJson]
[RoutePrefix("api")]
public class MyController : ApiController
{
    // ...
}