Mapping _escaped_fragment_ to controller actions

February 3, 2014

If you have a website which loads content dynamically using AJAX, you may be using hashbangs (#!) to indicate state. This method is popular due to the specification put together by google which translates the url fragment into a querystring parameter, and issues a request to the server to get an html snapshot of the state.

This is well documented by google, here: https://developers.google.com/webmasters/ajax-crawling/docs/specification.

In order to return the various states of the page, you need to respond to different values of the _escaped_fragment_ querystring parameter sent as part of a GET request to this page. Most implementations I’ve seen to deal with this end up switching on the value of this querystring parameter inside an Action method. Here is an example of what i’m talking about http://stackoverflow.com/questions/5643257/implementing-googles-hashbang-ajax-crawl-with-asp-net-mvc.

Below is an implementation of an EscapedFragmentRoute which allows you to map routes which include hashbangs to actions using the standard MVC routing infrastructure. Just include the hashbang in your route patterns, and this will match it, working off the assumption that the url fragment can be found in the _escaped_fragment_ querystring parameter.

    public class EscapedFragmentRoute : Route
    {
        private const string EscapedFragmentQsParameter = "_escaped_fragment_";

        public EscapedFragmentRoute(Route baseRoute)
            : base(baseRoute.Url, baseRoute.Defaults, baseRoute.Constraints, baseRoute.DataTokens, baseRoute.RouteHandler)
        {
        }

        public override RouteData GetRouteData(HttpContextBase httpContext)
        {
            var fragment = httpContext.Request.QueryString[EscapedFragmentQsParameter];

            if (fragment != null)
                return base.GetRouteData(new HttpContextBaseWrapper(httpContext, fragment));

            return base.GetRouteData(httpContext);
        }

        public class HttpContextBaseWrapper : HttpContextBase
        {
            private readonly HttpContextBase httpContextBase;
            private readonly HttpRequestWrapper httpRequestWrapper;

            public HttpContextBaseWrapper(HttpContextBase httpContextBase, string fragment)
            {
                this.httpContextBase = httpContextBase;
                this.httpRequestWrapper = new HttpRequestWrapper(httpContextBase.Request, fragment);
            }

            public override HttpRequestBase Request
            {
                get { return this.httpRequestWrapper; }
            }

            public override HttpResponseBase Response
            {
                get { return this.httpContextBase.Response; }
            }

            public override IDictionary Items
            {
                get { return this.httpContextBase.Items; }
            }

            public override IPrincipal User
            {
                get
                {
                    return this.httpContextBase.User;
                }

                set
                {
                    this.httpContextBase.User = value;
                }
            }
        }

        public class HttpRequestWrapper : HttpRequestBase
        {
            private readonly HttpRequestBase request;

            private readonly string fragment;

            public HttpRequestWrapper(HttpRequestBase request, string fragment)
            {
                this.request = request;
                this.fragment = fragment;
            }

            public override string AppRelativeCurrentExecutionFilePath
            {
                get
                {
                    return this.request.AppRelativeCurrentExecutionFilePath;
                }
            }

            public override string PathInfo
            {
                get
                {
                    return this.request.PathInfo + "#!" + this.fragment;
                }
            }
        }
    }

I am using attribute routing with MVC 5.1 which allows you to write your own route generation attributes. The following attribute creates an EscapedFragmentRoute instead of the standard one. Note, if you go down this path, then you need to use this attribute on the main page too, otherwise you’ll find that multiple controllers/actions may match the url, and cause an error

    public class EscapedFragmentRouteAttribute : Attribute, IDirectRouteFactory
    {
        public EscapedFragmentRouteAttribute(string template)
        {
            if (template == null)
                throw new ArgumentNullException("template");

            this.Template = template;
        }

        public string Name { get; set; }

        public int Order { get; set; }

        public string Template { get; private set; }

        public RouteEntry CreateRoute(DirectRouteFactoryContext context)
        {
            var builder = context.CreateBuilder(this.Template);

            builder.Name = this.Name;
            builder.Order = this.Order;
            var routeEntry = builder.Build();

            return new RouteEntry(routeEntry.Name, new EscapedFragmentRoute(routeEntry.Route));
        }
    }

Finally, a quick example of how to use this:

    [RoutePrefix("#!/recipe")]
    public class RecipeController : AsyncController
    {
        [EscapedFragmentRoute("{recipeId}")]
        public async Task<ActionResult> Details(int recipeId)
        {
            ...
        }
    }