Tuesday, January 28, 2014

Route Base

Route Base - By Narendra Shrestha


In our previous thread, we discussed about custom route handler in context to the situations when you need to deflect from MVC Standards i.e. MVC’s own “MvcRouteHandler”. Continuing our previous work, let us shift one step further. We are totally fine with our present system. Our system happily accepts both kind of URL patterns i.e. MVC like URL pattern ({controller}/ {action}) & file system like URL pattern (*.aspx). User entering URLs “Grid/Food” and “/Grid/MyWebForm.aspx?Department=Food” gets the same page rendered. In ever changing world of software, where requirement piles up each day, we get a new requirement.

The requirement is 

“When user enters the legacy URL i.e. file system like URL, the browsers should no longer display the legacy URLs in address bars. Instead new URL format should be improvised. However, if User enters or has legacy URL bookmarked, then he/she should have no problem accessing the source”.
According to the above requirement, we can no longer display URLs like “/Grid/MyWebForm.aspx?Department=Food” on the browsers addresses bars. Instead we should resort to URLs like “/Grid/Food”.

This is one of the critical transformations. But why would someone call for such changes? The reason is the easiness in reading out or making note of the URLs. We also mentioned it in our previous thread that short and sweet URLs improve the page ranking in the search engine. As long as you are choosing sensible terms in the URLs, you should have no complaints regarding any search engines whatsoever.
Vague planning of changing URLs can hamper your website many ways. First of all changing URLs is like killing your own business. You may have marketed your URLs at some point. But if we follow fall back architecture, everything should work fine. URLs are meant to change. It depends upon your marketing strategy, your perception to adhere to the changes and your ability to maintain reputation. The user shouldn’t feel that you’ve made changes. They would love it if you’ve trimmed your URLs for all the good reasons. For example: - it would be a treat to many Amazon users if the makers were little bit considerate about their URLs. 

There are various methods of supporting the Legacy URLs. One way is by creating our own class in infrastructure folder and deriving it from class “RouteBase”. We derive from “RouteBase” class when we prefer something else instead of MVC’s traditional way of matching URLs. We can have control over how URL pattern is matched and how outgoing URLs are generated. Thus, “RouteBase” is the perfect candidate for handling incoming and outgoing URLs.

For outbound URL, we are simply going to generate URLs by following our preferred format of the URLs.

The below listing shows the skeletal structure for the class deriving from “RouteBase

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Routing;

namespace RouteHandler.Infrastructure
{
    public class MyLegacyRoutes:RouteBase
    {
        public override RouteData GetRouteData(HttpContextBase httpContext)
        {
            throw new NotImplementedException();
        }

        public override VirtualPathData GetVirtualPath(RequestContext
         requestContext,RouteValueDictionary values)
        {
            throw new NotImplementedException();
        }
    }
}

Listing 1:- Skeletal structure of class derived from “RouteBase”class.

In order to access “RouteBase” class we need to refer to the namespace “System.Web.Routing” in the class.

We can see that our class “MyLegacyRoutes” has overridden two of the methods in the base class. They are “GetRouteData” and “GetvirtualPath”.

GetRouteData

The signature of the “GetRouteData” method is as shown below:-

public override RouteData GetRouteData(HttpContextBase httpContext)

By this method, the incoming URLs are matched according to our preference. We can use our own route handler or MVC’s own “MvcRouteHandler”. This method is called for each route in the route table until a non-null value is returned by this method.

This method accepts the parameter of type “HttpContextBase”. So without any problem we can access http request, response, sessions, cookies etc inside this method. In this method, we create a “RouteData” of any handler and add the routing values. Once we create our “RouteData”, we will return this value.

GetVirtualDataPath

The signature of the “GetVirtualPath” method is as shown below:-

public override VirtualPathData GetVirtualPath(RequestContext
         requestContext,RouteValueDictionary values)

By this method, the outbound URLs are generated. While generating the outbound URLs, frame work calls this method for each route in the route table until a non-null value is returned by this method.

Since this method accepts the parameter of type “RequestContext”, we can access the request’s “HttpContextBase” and “RouteData” parts. We can access the route value dictionary through the parameter “RouteValueDictionary”. We can create and return new instance of “VirtualPathData” inside this method by our preferred format to generate outbound URLs.

Turning back to our requirement, we make changes in our class “MyLegacyRoutes”. The following listing shows the same:-


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Routing;
using System.Web.Mvc;

namespace RouteHandler.Infrastructure
{
    public class MyLegacyRoutes : RouteBase
    {
        public string[] Urls;
        public MyLegacyRoutes(params string[] MyLegacyUrls)
        {
            Urls = MyLegacyUrls;
        }
        public override RouteData GetRouteData(HttpContextBase httpContext)
        {
            RouteData result = null;
            string requestedUrl = httpContext.Request.AppRelativeCurrentExecutionFilePath;
            if (Urls.Contains(requestedUrl, StringComparer.OrdinalIgnoreCase))
            {
                result = new RouteData(this, new MvcRouteHandler());
                result.Values.Add("controller", "Home");
                result.Values.Add("action", "LegacySupport");
                if (httpContext.Request.QueryString["Department"] != null)
                {
                    result.Values.Add("department", httpContext.Request.QueryString["Department"]);
                }
            }
            return result;
        }

        public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
        {
            VirtualPathData result = null;
            if (values.ContainsValue("LegacySupport"))
            {
                result = new VirtualPathData(this,
           new UrlHelper(requestContext).Content("Grid/"+(string)values["department"]).ToString());
            }
            return result;
        }
    }
}

Listing 2:- “MyLegacyRoutes” to generate inbound and outbound URLs.

Inside this derived class, there is a constructor in which we accept array of string. In our case, this array of string represents the list of legacy URLs which we are planning to handle. We supply this array right during the time of instantiation of this class. This is done while registering route which will follow shortly after this discussion.

Within the method “GetRouteData”, we query the URL requested by user by using “httpContext.Request.AppRelativeCurrentExecutionFilePath”. We check if this URL is contained within the array of supported legacy URLs. We ignore the URL case. Once, we find a positive match, we instantiate a “RouteData” for this “RouteBase” and for handler “MvcRouteHandler”. We use this particular handler because we want our request to be directed towards the “Home” controller’s “LegacySupport” action. In order to convey this information, we added values to our route data in form of key/value pair. We isolated the querystring and added it as one of the key/value pair in the route data. This value of querystring will be received in the “LegacySupport” action as a parameter named “department” of type string. Lastly, we return the route data result. In case, if there is no match, we simply return a null value.

Inside the method “GetVirtualPath”, we check if the route value dictionary contains value “LegacySupport” for key “action”. We do this because we want to make sure that we are dealing with legacy URLs. Then we create the new instance of the “VirtualPathData” for this “RouteBase”. We specify our URL pattern through “UrlHelper(requestContext).Content("Grid/"+(string)values["department"]).ToString()”.
For the current request, we generate outbound URLs of type “Grid/department”. The value of department is accessed through the route value dictionary. In case for other request, we simply return a null value.

After we create our own class by overriding the methods of “RouteBase”, it’s time to make some changes in the route registration in the file “Global.asax.cs”.

public static void RegisterRoutes(RouteCollection routes)
{
           
       routes.RouteExistingFiles = true;
       routes.Add(new MyLegacyRoutes("~/Grid/MyWebForm.aspx",”~/Grid/MyOtherWebForm.aspx”));
       Route myRoute = new Route("Grid/{Department}", new MyRouteHandler());
       routes.Add("MyRoute", myRoute);
       routes.MapRoute(
            "Default", // Route name
            "{controller}/{action}/{id}", // URL with parameters
   new { controller = "Home", action = "SelectDepartment", id = UrlParameter.Optional }
       );
}

Listing 3:-Registering routes

During registering routes, we added the route of type “MyLegacyRoutes”. We specified the legacy URLs to be ~/Grid/MyWebForm.aspx",”~/Grid/MyOtherWebForm.aspx”. Thus, for any request to the “MyWebForm.asxp” inside “Grid” folder, the “GetRouteData” will return non-null value.

Before registering routes, we set “routes.RouteExistingFiles = true”. By default routing system checks to see if the URLs match the disk file. If so, the disk files are served and routes we defined are never used. In order to prevent this default behavior we prioritize our routes rather than relying on the default behavior. If we hadn’t done this, we would be never taken to “MyLegacyRoutes” class for the request like “Grid/MyWebForm.aspx?Department=Clothing”. You need to be careful while doing so because URLs that match the disk files are not only “aspx” files (ex:-html, htm, xml etc.).

We add “LegacySupport” action in the “Home” controller so that we can direct the request towards it for legacy URLs. The following listing shows the same:-

public RedirectToRouteResult LegacySupport(string department)
{
     return RedirectToRoute("MyRoute", new { Department = department });
}

Listing 4:-The “LegacySupport” action

This action redirects to route named “MyRoute”. This is more or less similar with our existing action named “SelectDepartment”. We could have used the existing action. Since, the pre-existing action is for the request of type POST; it will not work for us. Also note that we return “RedirectToRouteResult” type instead of “ActionResult”. It won’t matter if we revert to the later type becuase “RedirectToRouteResult” is derived from “ActionResult”. We are only being more conservative about our return type.

Last but not least, we will add following to our view name “SelectDepartment.cshmtl” for demonstrating the generation of the outgoing URL.
.
.
.
<p>
    Url 1:@Html.ActionLink(Url.Action("LegacySupport", new { department = "Food" }).ToString(), "LegacySupport", new { department = "Food" })
</p>
<p>
    Url 2:@Html.ActionLink(Url.Action("LegacySupport", new { department = "Clothing" }).ToString(), "LegacySupport", new { department = "Clothing" })
</p>
.
.
.
Listing 5:- URL addition for demonstrating outbound URL in “SelectDepartment.cshtml”

We included two URLs namely “Urls 1” and “Url 2”.
Our URL is generated by two ways. First of all, we generate URL text by “Url.Action("LegacySupport", new { department = "Food" }).ToString()”. This causes the “GetVirtualPath” to return non-null value based on the format we specified. Then, @Html.ActionLink” binds this URL to the action “LegacySupport” in the “Home” controller. We supplied the route object as anonymous type so that they are taken as a parameter to the “LegacySupport” action.

Now if we directly make the request to the URLs like “/Grid/MyWebForm.aspx?Department=Food”, we will get the following output. Check the address bar for URL format like “Grid/Food”. Hence, we have met the requirement.



Figure 1:- implementation of new URL format or inbound URL

Following figure shows the outbound URLs generated for the view “SelectDepartment.cshtml”. If we click on any URL, we will be directly taken to the respective grids.



Figure 2:- implementation of outbound URL

Looking at the html code for the page,



Figure 3:- portion of html code for outbound URL in view “SelectDepartment.cshtml”

This has been possible because of the customization in the generation of the outbound and inbound URLs. There are various methods to perform the same implementation. My choice has purely been intentional because I want to demonstrate the use of “RouteBase”. We have benefited greatly from the flexibility of the MVC. This puts up MVC framework as one of the reliable platform to upgrade your old applications.