A simple redirection mechanism using MVC 5 [and IIS8], including “Legacy” URLs

For some hours, I have been looking for good ideas on how to manage redirections in MVC without necessarily configuring IIS (which is not always available to the programmer, unfortunately).

However, very few of the walkthroughs found on the web take into account dealing with “legacy” URLs; primarily, those who have the “.html” extension.

How do I, in MVC, redirect a URL like “oldfolder/oldfilename.html” to a new MVC URL as “/newfolder/beautifulpeople” ?

The answer is not the simplest, because MVC doesn’t manage all that is asked to it by the browser:

A client request is handled, summarily, like this:

– MVC manages the “MVC” routes (as “/area/controller/action”)

– IIS8 manages the static files, which *presumably* don’t need MVC’s intervention.

If we want to manage all the redirections via MVC, we have to tell MVC that it has to take care of BOTH MVC routes AND static files (unless, of course, the latter don’t exist as such. If the static file exists, let IIS transfer it to the user).

The following is a very basic mechanism which uses MVC and a DB table to store redirections. It can be extended to use caching and OR XML files to be faster. It is also true that redirections should be an exception rather than the norm.

The idea is:

– We create a DB table holding old URLs and corresponding new URLs, along with the code we want to use for the redirection (301 is permanent) and an “active” flag.

– We create a set of MVC “catchall” routes (in the RouteConfig file) which will prevent 404 (“not found”) and will call a new controller that does the “redirection” job

– This new controller checks if the DB has an entry equal to the originating URL. If there is, it redirects  the flow to the new URL (permanenty or temporarily, depending on what we wrote in the DB entry); otherwise, it generates a custom 404 page.

– We tell MVC (in web.config) that it has to also take care of the routes that look like files, unless (of course) they are actual static files.

RouteConfig

In our MVC application’s RouteConfig, we create, AFTER the standard routes, a set of “catchall” routes which will direct to a new controller and action (“Error”/”NotFound”) all the requests that could not be routed to an existing controller.

Like this:

//Default, Real route

routes.MapRoute(

name: “Default”,
url: “{controller}/{action}/{id}”,
defaults: new { controller = “Home”, action = “Index”, id = UrlParameter.Optional },
constraints: new { controller=”Home|Search|Account|Show|Other_Controllers|Error”} // CONSTRAINTS are VERY IMPORTANT
);

//catchAll routes to grab the URLs we want to redirect

routes.MapRoute(
name: “Redirect_OrError”,
url: “{*CatchAll1*}”,
defaults: new { controller = “Error”, action = “NotFound” }
);

routes.MapRoute(
name: “Redirect_OrError_two_levels”,
url: “{CatchAll1*}/{CatchAll2*}”,
defaults: new { controller = “Error”, action = “NotFound” }
);

routes.MapRoute(
name: “Redirect_OrError_three_levels”,
url: “{CatchAll1*}/{CatchAll2*}/{CatchAll3*}”,
defaults: new { controller = “Error”, action = “NotFound” }
);
routes.MapRoute(
name: “Redirect_OrError_four_levels”,
url: “{CatchAll1*}/{CatchAll2*}/{CatchAll3*}/{CatchAll4*}”,
defaults: new { controller = “Error”, action = “NotFound” }
);

Attention: The “default, real” MapRoute method must be called with Constraints (Controller=”Home|…. all the controllers you have). If you don’t do so, MVC will not fall through to our “catchall” controllers. It will stop at the default and will give a “404”.

The “Error” Controller

The “Error” Controller does three things:

1. It checks if the request is actually found in the Redirections table as a URL we consider as “old”

2. If it is there, we redirect to the new URL (if 301, “RedirectPermanent”; if others, “Redirect”)

3. If it doesn’t, it generates a custom error page with a 404 status code

public class ErrorController:Controller
{

[HttpGet]
public ActionResult NotFound()
{

string UrlToTest = Request.Url.AbsolutePath.Trim(‘/’);
Redirections redirect = RedirectManager.GetRedirectFromDB(UrlToTest);
if (redirect != null)
{
if (redirect.RedirectType==”301″)
{
return RedirectPermanent(redirect.NewURL);
}
else
{
return Redirect(redirect.NewURL);
}
}
Response.StatusCode = 404;
ViewBag.UrlToTest = UrlToTest;
return View();
}

public ErrorController()
{

}

}

Now what is the RedirectManager we just found in the code?

Very simply, it is a class which contains some helper classes and methods that allow us to examine the DB and look for the URL we want to redirect from.

public class RedirectManager

{

public RedirectManager()

{

}

public class RedirectionsDTO
{

public int Id { get; set; }
public string OldURL { get; set; }
public string NewURL { get; set; }
public string RedirectType { get; set; }
public Nullable<bool> Active { get; set; }

}

public static Redirections GetRedirectFromDB(string OldUrl)
{
RedirectionsDTO redirects;
using (var db = new OurEntitiesDB()) // this is the name of your Entity Framework
{
string URLWithoutSlash = OldUrl;
if (!URLWithoutSlash.StartsWith(“/”))
{
URLWithoutSlash = “/” + URLWithoutSlash;
}

redirects = (from e in db.Redirections
where (e.Active == true && (e.OldURL == URLWithoutSlash))
select new RedirectionsDTO
{
OldURL = e.OldURL,
NewURL = e.NewURL,
RedirectType=e.RedirectType,
Active=e.Active
}).FirstOrDefault();
if (redirects != null)
{
return new Redirections { OldURL = redirects.OldURL, NewURL = redirects.NewURL, Active=redirects.Active, RedirectType=redirects.RedirectType };
}
else
{
return null;
}

}

}

}

}

How is the Redirect table created? It is created by Entity Framework Database first, based on a table maybe similar to this…

MVC redirection storage table

Table which holds redirection data

There should be an index on the OldURL column because we will include it in our queries.

The generated code could be something like this:

namespace YourPreferredNameSpace

{
using System;
using System.Collections.Generic;

public partial class Redirections
{
public int Id { get; set; }
public string OldURL { get; set; }
public string NewURL { get; set; }
public string RedirectType { get; set; }
public Nullable<bool> Active { get; set; }
}
}

Of course, you have to fill this table with the needed redirections! I suggest you create a controller and allow scaffolding templates to automatically generate the CRUD actions and views for these “Redirections” objects.

Web.Config

What we did so far does not help us if we want to redirect *.html files (or *.pdf files, for that matter). This is beacuse IIS will think that the *.html file should not be a part of an MVC route and it will look for it as a static file. To allow MVC to see the *.html URL as a possible MVC route (and redirect it, if needed), we have to allow MVC to examine all the file requests. To do so, we have three suggested paths. There are: an easy+smart way, a smart way and a semi-silly way. The easy+smart way didn’t work for me. The “smart way” did. It happens.

The easy+smart way is to patch the IIS as this article suggests: http://support.microsoft.com/kb/980368

It didn’t work for me because I had no access to IIS.

The “smart one” is suggested by Colin Farr in this old but powerful article:

http://www.britishdeveloper.co.uk/2010/06/dont-use-modules-runallmanagedmodulesfo.html

he suggests that in System.webServers you add

<system.webServer>
<modules >
<remove name=”UrlRoutingModule-4.0″ />
<add name=”UrlRoutingModule-4.0″ type=”System.Web.Routing.UrlRoutingModule” preCondition=”” />

… other modules…

</module>

Now the silly solution instead:

add the runAllManagedModulesForAllRequests=”true” attribute to the “module” section. Colin Farr already explains why this is silly, so I will not.

<modules runAllManagedModulesForAllRequests=”true”>

URL routing in Microsoft MVC 5 when Areas are present: potential pitfalls

The “convention over configuration” philosophy of Microsoft’s MVC engine is very useful, as it automates a lot of “wiring” among the different components of a web application.

However, when the “going gets tough”, you have be careful how to circumvent certain convention-based automations that may bring undesired results.

One very important aspect to consider is routing, which technically is not part of MVC (it is a separate component of .net, as you can also use it with web forms) but plays an incredibly important role in the functioning of MVC. Routing is the mechanism that calls a certain object (in MVC, it is usually a controller) based on the URL the user typed or clicked on.

In MVC’s views based on Razor 3, we have a very user HTML helper which creates a URL based on:

– the controller we need

– the action we need.

Its syntax is the following:

@Html.ActionLink(“link text”, “action name”, “controller name”)

When you divide the application in Areas, you have to be extra-careful, as the overload that allows you to specify the Area you need has five arguments rather than four as you might wonder (since the Area is the fourth argument.)

So, in case you need a link that should call a controller inside an Area, it should read:

@Html.ActionLink(“link text”, “action name”, “controller name”, new {Area=”Area Name”}, new {})

The last new{} parameter is in lieu of html attributes. Without that specification, the compiler will think we’re calling the four-parameter overload, which has as fourth parameter… exactly the html attributes, not the Routing object and we would be wrong.

Is that all? No. There is another consideration related to the priority that the routing mechanism uses when you register more than one route in an Area route registration class (or any registration class, for that matter.)

When you create an area, in fact, an additional route AreaRegistration subclass is created for you.

For instance: if you create an area called “Particularities”, Visual Studio will create a class with this code (could not reproduce the indentation correctly here):
using System.Web.Mvc;
namespace RMTestSite.Areas.Particularities
{
public class ParticularitiesAreaRegistration : AreaRegistration
{
public override string AreaName
{
  get
{
  return "Particularities";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
  context.MapRoute(
  "Particularities_default",
  "Particularities/{controller}/{action}/{id}",
   new { action = "Index", id = UrlParameter.Optional }
 );
}
}
}

So, if you want to point to the new Area controller from a page called by a controller that is in another Area, you would have to code:

@Html.ActionLink("Particularities","Index", "Home", new{Area="Particularities"}, new {})

The result of this link is an reference to:
http://localhost:53869/Particularities/Home, which is OK.

Now let us imagine we add another route to the Particularities Area.

For instance, let us add this route before our default route (as a rule, specialized routes should precede general routing rules):

context.MapRoute(
"Particularities_AdditionalParticularitiesdefault",
"Particularities/AdditionalParticularities/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);

What happens now if I use again my Html.ActionLink? I mean: if I use exactly again,
@Html.ActionLink("Particularities","Index", "Home", new{Area="Particularities"}, new {})

The result is different. The address becomes
http://localhost:53869/Particularities/AdditionalParticularities/Home

Why is this? Because the routing mechanism found the first (in terms of: how the code is ordered) pattern in the route series which is matched by our controller/action ActionLink request. It does not choose the “simplest” one.

How can we continue to point to the previous controller/page?

We can specify the route itself when we build our link.

We can do this very easily by using RouteLink rather than the ActionLink. Some say this is in general a preferred routing habit: you can have thusly a list of what routes should do regardless where you want to implement the controllers and you are safer (well, in this case we would have been safe.)

Following our example, our code should have been
@Html.RouteLink("Particularities!", "Particularities_default", new { controller = "home", action = "index" }, new { })

We added the route name to the link, rather than the Area name, and we know we won’t get the wrong controller.

All in all, MVC’s routing mechanism (I specify, again, that is not a part of MVC per se) is a great thing, but we never have to give for granted that it will work in the way we guess a convention should work: it is the .net framework developer’s convention, not necessarily the one we think it is.