Skip to main content

Microsoft

Sitecore MVC – Globally-Templated Attribute Routes

I’ve worked on a few projects that make use of John West‘s technique to create AJAX services for your Sitecore components, as outlined in his blog post here. In a fresh Sitecore project, the action methods on your controllers are accessible from Sitecore only–you can’t access them by navigating to http://{{yoursiteurl}}/{controller}/{action} like you would in a standard MVC project. In his blog post, John West shows you how to make your action methods publicly accessible by setting up a default route in your project.
What I like about setting up a route as described in John West‘s post is that all of your AJAX services are accessible from a uniform URL. If you set your route as {controller}/{action}/{id}, all of your AJAX service URLs will follow that convention. However, registering a route like this makes all of the controller actions in your project publicly accessible, and I prefer not to expose any more external interfaces to my applications than absolutely necessary.
The best way to prevent all of your controller actions from being publicly accessible is by taking advantage of the Attribute Routing feature introduced with ASP.NET MVC 5. However, if you go through and decorate all of your controller actions with the Route attribute, you’ll quickly find yourself repeating your route template over and over again:

public class DemoController : Controller
{
  [Route("sitecoredemoapi/demo/demodata")]
  public ActionResult DemoData()
  {
    var demoContent = DateTime.Today.ToShortDateString();
    return Content(demoContent);
  }
}

You can reduce some of the duplication at the action level by applying the RoutePrefix attribute at the controller level:

[RoutePrefix("sitecoredemoapi/demo")]
public class DemoController : Controller
{
  [Route("demodata")]
  public ActionResult DemoData()
  {
    var demoContent = DateTime.Today.ToShortDateString();
    return Content(demoContent);
  }
}

But you’ll still find yourself repeating the same [RoutePrefix("sitecoredemoapi/{controllerName}")] on all of your controllers, and then [Route("{actionName}")] on the actions you want to be externally-facing.
In this post I will highlight a hybrid approach that will allow you to declare a routing template globally, like with standard MVC routing, and then opt in to that route at the action or controller level with a custom attribute, like with attribute routing.

Create the Attribute

using System;
namespace SitecoreDemo
{
  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)]
  public class PublicRouteAttribute : Attribute
  {
  }
}

The first step is to create your attribute. I called it PublicRouteAttribute so that anyone who sees the [PublicRoute] attribute on my actions or controllers will immediately know that the action or all of the controller’s actions are external-facing. I also opted to make the attribute applicable to both classes and methods so that if I create a controller dedicated to AJAX services I can apply the attribute once at the controller level instead of on all of my actions.

Create the RouteProvider

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.Mvc.Routing;
namespace SitecoreDemo
{
  public class PublicRouteProvider : DefaultDirectRouteProvider
  {
    private readonly string _routePrefix;
    public PublicRouteProvider(string routePrefix)
    {
      if (routePrefix == null) throw new ArgumentNullException(nameof(routePrefix));
      _routePrefix = routePrefix;
    }
    protected override string GetRoutePrefix(ControllerDescriptor controllerDescriptor)
    {
      return $"{_routePrefix}/{controllerDescriptor.ControllerName}";
    }
    protected override IReadOnlyList<IDirectRouteFactory> GetActionRouteFactories(ActionDescriptor actionDescriptor)
    {
      var publiclyRoutable = actionDescriptor.GetCustomAttributes(typeof(PublicRouteAttribute), false).Any();
      return publiclyRoutable ? new[] { new RouteAttribute(actionDescriptor.ActionName) } : null;
    }
    protected override IReadOnlyList<IDirectRouteFactory> GetControllerRouteFactories(ControllerDescriptor controllerDescriptor)
    {
      var publiclyRoutable = controllerDescriptor.GetCustomAttributes(typeof(PublicRouteAttribute), false).Any();
      return publiclyRoutable ? new[] { new RouteAttribute("{action}") } : null;
    }
  }
}

The next step is to create the RouteProvider. This RouteProvider takes as a constructor parameter the prefix that you want applied to all of your routes. It then searches for all controllers and actions in your project that are decorated with the [PublicRoute] attribute and creates routes for those actions that take the form of {routePrefix}/{controller}/{action}.
In the GetRoutePrefix method, the PublicRouteProvider effectively applies the RoutePrefix attribute to all of the controllers as if you had manually decorated them with [RoutePrefix("{routePrefix}/{controllerName}")].
In the GetActionRouteFactories method, the PublicRouteProvider finds all actions in your project that are decorated with the PublicRoute attribute and effectively treats the action as if you had decorated it with the Route attribute, [Route("{actionName}")].
In the GetControllerRouteFactories method, the PublicRouteProvider finds all controllers in your project that are decorated with the PublicRoute attribute and effectively treats the controller as if you had decorated it with the Route attribute, [Route("{action}")].

Register the RouteProvider

using System.Web.Mvc;
using System.Web.Routing;
using Sitecore.Pipelines;
namespace SitecoreDemo
{
  public class InitializeRoutes
  {
    public virtual void Process(PipelineArgs args)
    {
      RegisterRoutes(RouteTable.Routes);
    }
    protected virtual void RegisterRoutes(RouteCollection routes)
    {
      routes.MapMvcAttributeRoutes(new PublicRouteProvider("sitecoredemoapi"));
    }
  }
}

Register the PublicRouteProvider with MVC using the MapMvcAttributeRoutes extension method. As described above, this code will create routes in the form of sitecoredemoapi/{controller}/{action} for all controllers and action methods that are decorated with the PublicRoute attribute.
Warning: Sitecore uses the prefix api for its own API routes so you should not use that as the prefix when you register the PublicRouteProvider. Instead I suggest using a prefix such as yourprojectnameapi. Using the prefix api will cause issues with your Sitecore project.

Plug Routes into Sitecore Pipeline

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <pipelines>
      <initialize>
        <processor type="SitecoreDemo.InitializeRoutes, SitecoreDemo" patch:after="processor[@type='Sitecore.Mvc.Pipelines.Loader.InitializeGlobalFilters, Sitecore.Mvc']"/>
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

Plug the routes into the Sitecore initialize pipeline right after the Sitecore global filters are initialized.

Use the Attribute!

using System;
using System.Web.Mvc;
namespace SitecoreDemo.Controllers
{
  public class DemoController : Controller
  {
    public ActionResult SitecoreData()
    {
      var demoContent = DateTime.Today;
      return View(demoContent);
    }
    [PublicRoute]
    public ActionResult DemoData()
    {
      var demoContent = DateTime.Today.ToShortDateString();
      return Content(demoContent);
    }
  }
}

Now you can make your AJAX services externally accessible just by putting the [PublicRoute] attribute on top of your action methods or controllers as seen above. From within my views I can make AJAX calls to my DemoData method at http://{{mysiteurl}}/sitecoredemoapi/demo/demodata and get today’s date.

Closing Thoughts

This technique has worked out great for my team on my most recent project. One of my favorite things about ASP.NET MVC is how extensible the framework is and the routing subsystem is no exception. Do you make use of attribute routing on your Sitecore projects? If so, what conventions do you use? Let me know in the comments.

Thoughts on “Sitecore MVC – Globally-Templated Attribute Routes”

  1. Thanks Corey. Could you tell me how to capture the {id} part of the url, too?
    eg: http://{{mysiteurl}}/sitecoredemoapi/demo/demodata/1234
    Cheers!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Corey Smith, Sitecore MVP & Lead Technical Consultant

I'm a Sitecore MVP and Lead Technical Consultant at Perficient focusing on Sitecore and custom .NET development.

More from this Author

Categories
Follow Us
TwitterLinkedinFacebookYoutubeInstagram