How to create multiple 301 redirect urls in ASP.NET MVC

redirects

While ASP.NET MVC has a powerful routing engine for handling requests, there can be a scenario when you need to process a list of specific URL rewrites in your application. Often this can be due to a new version of an existing site going live where the URL structure has changed. Under Apache, this can be handled easily in the .htaccess file of the site by listing out each path  you'd like to rewrite like so:

<IfModule mod_rewrite.c>
  RewriteEngine on
  Redirect 301 /somepage.html /otherpage.html
</IfModule>

It can also be handled in IIS 7 using the URL Rewrite module or under IIS 6 using a 3rd party extension such as Helicon ISAPI Rewrite.

But what if you want to handle the redirects via code?

Bulk 301 Redirect - Considerations

Our two main considerations when coming up with this solution were:

  1. We don't want to check for a redirect on every request (just old broken links)*
  2. We don't want to have to do a code push to change the 301 mapping list

*: You can use the same solution to process every request by changing the location of the 301 check

This led us down a path where we are capturing 404 requests in the application, then checking for a rewrite rule before actually displaying a 404 page. If the rule exists, we rewrite the path instead of showing the 404.

The 301 List

With consideration #2 in mind, we decided to use a flat file in the App_Data folder to hold our rewrite rules. The file is simple since all we are concerned with is permanent redirects. We use a plain old CSV file with 2 columns, the first column being the old path to check for, the second column is the new path to permanently redirect to. For example:

/Home/FollowUs,/About/Follow-Us
/Home/ContactUs,/About/contact-us

This makes constructing, updating, and maintaining the rewrite list extremely easy. Even better, since it's a CSV file, you can use excel to have a non technical person create the list for you if it's lengthy.

To process the list, we created a single static method to load the list into a key,value Dictionary object. Here is the entire class:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;

namespace CypressNorth.Data.Services
{
    public class FlatFileAccess
    {
        public static Dictionary<string,string> Read301CSV()
        {
            string file = "App_Data/301.csv";
            string path = System.IO.Path.Combine(AppDomain.CurrentDomain.GetData("DataDirectory").ToString(), file);

            if (File.Exists(path))
            {
                using (TextReader sr = new StreamReader(path))
                {
                    Dictionary<string,string> redirect_dict = new Dictionary<string,string>();
                    string line = "";
                    while ((line = sr.ReadLine()) != null)
                    {
                        string[] columns = line.Split(',');
                        redirect_dict.Add(columns[0], columns[1]);
                    }
                    return redirect_dict;
                }
            }
            else
                return null;

        }

    }
}

This will process your list of rewrite rules into a Dictionary<string,string> list and return it for you.

Intercepting 404 responses

Now that we have our list of 301 redirects and a way to access them, we need to hook into the app request life cycle to process them. Our consideration #1 states that we don't want to process every single request to the site, just bad URL's so the logical place for this is when a 404 response is encountered.

To hook into this we create a new method in the global.asax file. You could do this via an HttpModule as well if you prefer.

protected void Application_Error()
{
	Exception exception = Server.GetLastError();
	// Clear the error
	Response.Clear();
	Server.ClearError();

	if (exception is HttpException)
	{
		HttpException ex = exception as HttpException;
		if(ex.GetHttpCode() == 404)
		{
			var url = HttpContext.Current.Request.Path.TrimEnd('/');
			var redirect = FlatFileAccess.Read301CSV();

			if (redirect.Keys.Contains(url))
			{
				// 301 it to the new url
				Response.RedirectPermanent(redirect[url]);
			}
		}
	}
}

This method will catch Exceptions application wide. You can implement much more robust error handing using the same hook but for our purposes we just want to catch 404's and check for redirects. So when an error is thrown in the application, the exception is checked to see if it is a 404 type HttpException. If it is, we load the 301.csv file and compare it against the requested path. If a match is found we 301 redirect the request to the new url. If it is not found, we let the error fall through.

The Result

You now have a simple, easy to maintain solution to manage your URL rewrites under ASP.NET MVC. If you're interested you can download the CypressNorth.Bulk301Redirect.Example.

4 Comments

  1. Author's Headshot
    phil April 27, 2012
    Reply

    Interesting solution but what are the major advantages over implementing this over using the URL Rewrite Module or Helicons ISAPI rewriter?

    I have a large legacy site whereby I'd prefer to implement rewrites in configuration rather than changing the code.

    • Author's Headshot
      Matt Mombrea April 27, 2012

      I wouldn't call them major advantages but some of the benefits of implementing rewrites this way are:

      -Non technical maintenance (for instance, an SEO department can construct the redirects in excel)

      -Rewrite updates with no code changes or server changes simply by uploading the new .csv file

      -Faster rewrite creation when you're dealing with a large number of 1 to 1 redirects

      Those were the main reasons we used this method on a particular product. We still do more advanced regex based rewriting using the Rewrite Module as well as the MVC routing engine, but creating a new route or a new rewrite entry for every 1 to 1 redirect would have taken much longer as we had around 100.

      I definitely don't see this approach as a replacement for rewrite modules, but it can be a helpful complement.

  2. Author's Headshot
    Lars Holdgaard May 16, 2012
    Reply

    Thanks so much.. I really needed this solution as I've to edit my URL routings .. And I really feared that would be a *****.

    Thanks!

  3. Author's Headshot
    Greg June 18, 2013
    Reply

    I know this is an older blog, but it was still helpful for me! For my site I added a cache dependency to the CSV reader. I haven't measured how much it will actually help, but it seemed like a wise modification:

    // First attempt to get it from the cache
    Dictionary dict = HttpRuntime.Cache["301-redirects"] as Dictionary;
    if (dict != null)
    return dict;

    // 301 list isn't cached, so read it from the CSV
    // ...however you like to build your Dictionary here...

    // Add the Dictionary to the cache with a dependency on the CSV file
    HttpRuntime.Cache.Insert("301-redirects", dict, new CacheDependency(HttpContext.Current.Server.MapPath("YOUR CSV PATH")));

    return dict;

Leave a Reply

Your email address will not be published. Required fields are marked *

Meet the Author

mmombrea-headshot
CTO / Partner

Matthew Mombrea

Matt is our Chief Technology Officer and one of the founders of our agency. He started Cypress North in 2010 with Greg Finn, and now leads our Buffalo office. As the head of our development team, Matt oversees all of our technical strategy and software and systems design efforts.

With more than 19 years of software engineering experience, Matt has the knowledge and expertise to help our clients find solutions that will solve their problems and help them reach their goals. He is dedicated to doing things the right way and finding the right custom solution for each client, all while accounting for long-term maintainability and technical debt.

Matt is a Buffalo native and graduated from St. Bonaventure University, where he studied computer science.

When he’s not at work, Matt enjoys spending time with his kids and his dog. He also likes to golf, snowboard, and roast coffee.