Overriding Sitecore 8 item resolution using MVC routing

If you're familiar with .NET MVC, you'll know how MVC routing can be used to consume the structure and elements of a URL path and parameters to resolve to a particular Controller class and Action method. This also takes care with ensuring that the requisite parameters are extracted from the URL and passed to the Controller.

If you're familiar with Sitecore MVC, you'll know that each rendering on a Sitecore page/item resolves to its own Controller, enabling dynamic composition of page output from multiple Controller classes. Routing-wise, Sitecore MVC works very much like Web Forms versions of Sitecore - i.e. the structure and elements of the URL path are mapped pretty much directly onto the Sitecore item tree in order to resolve the "context" item.

What if you'd like to combine some of each of the above behaviours? Perhaps you have a Sitecore page which displays some different state depending on a query string parameter (you can imagine this be be something like a product details page) - so normally you would address such a page with requests such as /products/product?code=345. This, as expected, resolves to your 'product' Sitecore item which lives at /sitecore/content/Home/products/product. What if you'd like to use the magic of MVC routing to be able to serve this page at /prod?code=345 instead? Or, let's go one step further and insert the parameter into the URL path instead of a query string parameter, so requests will be served via /prod/345, /prod/678 etc. However, we don't want to route to an MVC controller directly, like you would normally see with MVC routing - instead we want to continue to use Sitecore's rendering engine, effectively calling all of the Controllers pointed at by renderings on the /sitecore/content/Home/products/product item, as usual.

Note that the example which I'm about to show was concocted in a vanilla installation of Sitecore 8.1, revision 160302.

Your first step is to create the new route, using a Pipeline processor, try something like this:

using System.Web.Mvc;
using System.Web.Routing;
using Sitecore.Pipelines;

namespace RedMoon
{
    public class RegisterExampleRoute
    {
        public virtual void Process(PipelineArgs args)
        {
            RouteTable.Routes.MapRoute("MvcTestRoute", "prod/{*code}",
                new
                {
                    scItemPath = "/sitecore/content/Home/products/product",
                    controller = "Sitecore",
                    code = UrlParameter.Optional
                });
        }
    }
}

This broadly says - if the URL path starts with 'prod/', please use the Sitecore controller (this is the 'default' controller which Sitecore uses behind-the-scenes with Sitecore MVC) but use "/sitecore/content/Home/products/product" as the context Sitecore item, rather than finding the item based on the URL path (which is what would normally happen.) Additionally, if any text follows 'prod/' in the URL, pass this in as a Controller parameter with the name 'code'.

The next step is to patch in this pipeline processor at the correct point: You want the processor to run before the out-of-the-box Sitecore.Mvc.Pipelines.Loader.InitializeRoutes processsor, so that requests are evalutated to see of your custom routing should be applied, before the standard Sitecore routing kicks in. Something along these lines should work (although remember that Sitecore 'include' config files are evaluated in alphabetical order, so you may need to give your patch file an appropriate name and/or put it in an appropriately named folder in order for it to appear in the correct place in the final configuration. Always check /sitecore/admin/showconfig.aspx to ensure that your pipeline processor is appearing where you expect it to!)

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" >
  <sitecore>
	  <pipelines>
		  <initialize>
			<processor type="RedMoon.RegisterExampleRoute"
			  patch:before="processor[@type='Sitecore.Mvc.Pipelines.Loader.InitializeRoutes, Sitecore.Mvc']" />
		  </initialize>
		</pipelines>
	</sitecore>
</configuration>

To test the parameter passing I added a View Rendering to the presentation of my /sitecore/content/Home/products/product item, which contained the following:

<p>

"code" query string value:
@Request.QueryString["code"]
<br />

"code" MVC route value:
@(ViewContext.RouteData.Values["code"])

<p>

If I make a request using the 'old' URL format - /products/product?code=345 - I can see that the Request query string is being set as expected. If I instead request /prod/1234, I can see that this also resolves to the product page. Also, I can see that the 'code' parameter is being passed to the view (i.e. a Controller rendering would be able to use this value as an input parameter). However it's worth nothing that the Request object is not changed, i.e. the second approach means that the 'code' parameter is not available as a query string parameter (which shouldn't be a problem if you get the parameter as Controller parameter rather than from interrogating the HttpContext directly.)

Of course another way to achieve the kind of item resolution outlined above is to use a custom ItemResolver, which allows you to add any extra processing you need to (for example, you could use HttpContext.RewritePath to plug your 'code' parameter into the HttpContext as a 'real' query string, if you so desired.) You can also use more complicated logic to decide which requests you want to intercept, and which you want to let fall through to Sitecore's default ItemResolver. Sitecore's built-in 'Aliases' functionality provides yet another way of manipulating item resolution, albeit a more rudimentary one.

Whichever approach you use, be wary of interfering with the way Sitecore is 'supposed' to work. For instance, the MVC-style solution I tried *relies* on there being an item at '/sitecore/content/Home/products/product' - if this item ever got renamed/moved/deleted then it wouldn't work! Also have a think about whether you need to consider language-specific URLs, as the example only works in English! Use at your own risk :)

There's also a video version of this post here...


By James at 22 Jan 2017, 16:45 PM


Comments

Post a comment