X

Register now for unlimited access to Sitecore resources.


Already have an account? Log in now

*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
X

Request a demo

It’s easy to get started. Sign up for a personalized demo.

*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
Sitecore Blog: @sitecorejohn blog

Fun Facts about MVC Route Initialization in the Sitecore ASP.NET CMS

By John West, November 09, 2012 | Rating:  | Leave a comment

This blog post describes modifications that the Sitecore ASP.NET web Content Management System (CMS) makes to MVC route definitions and provides sample code to prevent the system from making those changes to specific routes.

The release of Sitecore 6.6 adds the Sitecore.Mvc.Routing.RouteHandlerWrapper class and Sitecore.Mvc.Routing.RouteHttpHandler classes that were not present in the technology preview. The RouteHandlerWrapper class implements the System.Web.Routing.IRouteHandler interface. The InitializeRoutes processor that Sitecore MVC adds to the initialize pipeline replaces the route handlers for most of the defined routes with the RouteHandlerWrapper, which…wraps the existing route handlers.

When MVC calls the GetHttpHandler() method of the route, RouteHandlerWrapper returns an instance of  RouteHttpHandler that wraps the default IHttpHandler returned by the GetHttpHandler() method in the wrapped route. The BeginProcessRequest() method of RouteHttpHandler invokes the mvc.beginRequest pipeline and then the BeginProcessRequest() method of the wrapped IHttpHandler, and EndProcessRequest() methods of the RouteHttpHandler invokes the EndProcessRequest() method of the wrapped IHttpHandler and then the mvc.endRequest pipeline.

Technically, the InitializeRoutes processor only adds the wrapper for routes that define defaults. For example, the third argument in this route defines some defaults:

RouteTable.Routes.MapRoute(
  name: "Mvc",
  url: "Mvc/{action}/{id}",
  defaults: new { controller = "Mvc", action = "Index", id = UrlParameter.Optional });

This route does not define any defaults, so Sitecore does not wrap it:

RouteTable.Routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

This is important because we do not want Sitecore to invoke those pipelines for HTTP requests that the solution does not handle with MVC, such as those with URLs that match this route.

This may not be very interesting - in fact it simply addresses a defect in the technology preview that prevented Sitecore from invoking the mvc.requestEnd pipeline in some cases - but it shows one place you could go to implement something similar (some logic to apply on every MVC request). If you find cases where you do not want Sitecore to apply these wrappers, you can define a token (scNoRouteHandler in the example below) in the defaults list and override the InitializeRoutes processor to apply the wrapper logic only if that token is not present or is false. For example, I am experimenting with ASP.NET MVC 4 (not currently supported by MVC, though I am pushing product management for this support - please contact your Sitecore representatives and demand support for MVC 4) and Web API, and I am not sure whether this wrapping logic has adverse effects on Web API routes. Note that you can probably use Web API without MVC or with MVC 3.

Additionally, I do not yet understand why, but if I register a Web API route after the Sitecore catchall route, it works, even though I thought that ASP.NET would apply the first route in the route table that matches the URL, and the catchall route should match the URL of the Web API call. Update 9.November.2012: The Sitecore Default MVC Route is NOT a Catchall Route. But if I register my Web API route before the InitializeRoutes processor, Sitecore applies a default action for the route. This is another case where I can override InitializeRoutes and add another token (scNoAction) to prevent Sitecore from making this change to the route.

Here is an example of using these tokens in a Web API route:

GlobalConfiguration.Configuration.Routes.MapHttpRoute(
  name: "DefaultApi",
  routeTemplate: "api/{controller}/{id}",
  defaults: new { id = RouteParameter.Optional, scNoRouteHandler = true, scNoAction = true });

Unfortunately, I had to copy some of the private methods from the InitializeRoutes processor into my override, so this is a little longer than it should need to be, but here is my untested override of the InitializeRoutes processor:

namespace Sitecore.Sharedsource.Mvc.Pipelines.Loader
{
  using System;
  using System.Web.Routing;
  
  using SC = Sitecore;
  
  public class InitializeRoutes : Sitecore.Mvc.Pipelines.Loader.InitializeRoutes
  {
    private void AddDefaultRouteValue(
      string key, 
      object value, 
      RouteValueDictionary defaults)
    {
      if (!defaults.ContainsKey(key))
      {
        defaults.Add(key, value);
      }
    }
  
    private void AddKeyToIgnore(
      string key, 
      RouteValueDictionary defaults)
    {
      this.AddDefaultRouteValue(
        SC.Mvc.Configuration.MvcSettings.IgnoreKeyPrefix + key, 
        string.Empty, 
        defaults);
    }
  
    private void AddSpecifiedIgnoreKeys(RouteValueDictionary defaults)
    {
      Action<string> action = null;
      string[] source = defaults["scKeysToIgnore"] as string[];
      string str = defaults["scKeysToIgnore"] as string;
  
      if (source != null)
      {
        if (action == null)
        {
          action = key => this.AddKeyToIgnore(key, defaults);
        }
  
        Sitecore.Mvc.Extensions.EnumerableExtensions.Each<string>(source, action);
      }
  
      if (str != null)
      {
        this.AddKeyToIgnore(str, defaults);
      }
    }
  
    protected override void SetRouteHandlers(
      RouteCollection routes, 
      SC.Pipelines.PipelineArgs args)
    {
      foreach (RouteBase routeBase in routes)
      {
        Route route = routeBase as Route;
  
        if (route != null)
        {
          IRouteHandler routeHandler = route.RouteHandler;
  
          if (routeHandler != null)
          {
            RouteValueDictionary defaults = SC.Mvc.Extensions.ObjectExtensions.ValueOrDefault<Route, RouteValueDictionary>(
              routeBase as Route,
              r => r.Defaults);
  
            if (defaults != null)
            {
              if (defaults["scNoRouteHandler"] == null
                || !(bool)defaults["scNoRouteHandler"])
              {
                route.RouteHandler = new SC.Mvc.Routing.RouteHandlerWrapper(
                  routeHandler);
              }
            }
          }
        }
      }
    }
  
    protected override void SetDefaultValues(
      RouteCollection routes,
      SC.Pipelines.PipelineArgs args)
    {
      SC.Diagnostics.Assert.ArgumentNotNull(routes, "routes");
  
      foreach (RouteBase routeBase in routes)
      {
        RouteValueDictionary defaults = SC.Mvc.Extensions.ObjectExtensions.ValueOrDefault<Route, RouteValueDictionary>(
          routeBase as Route,
          r => r.Defaults);
  
        if (defaults != null)
        {
          if (defaults["scNoAction"] == null
            || !(bool)defaults["scNoAction"])
          {
            this.AddDefaultRouteValue(
              "action",
              SC.Mvc.Configuration.MvcSettings.SitecoreActionName,
              defaults);
          }
  
          this.AddDefaultRouteValue(
            "controller",
            SC.Mvc.Configuration.MvcSettings.SitecoreControllerName,
            defaults);
          this.AddKeyToIgnore(
            "action",
            defaults);
          this.AddKeyToIgnore(
            "controller",
            defaults);
          this.AddKeyToIgnore(
            "scItemPath",
            defaults);
          this.AddKeyToIgnore(
            "scLanguage",
            defaults);
          this.AddSpecifiedIgnoreKeys(
            defaults);
        }
      }
    }
  }
}

The important stuff is in the SetRouteHandlers() and SetDefaultValues() methods.

Here is a Web.config include file (Sitecore.Shardsource.Mvc.InitializeRoutes.config in my case) to override the default InitializeRoutes processor in the initialize pipeline with the prototype above:

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

Continued thanks to Kern Herskind Nightingale of Sitecore for helping me get my MVC bearings.

Resources

 

Tags: API, Architecture, Infrastructure, Integration, MVC

Commenting is temporary disabled due to maintenance work.

*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.