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.