Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Handling 301 Redirects in ASP.NET 3.5

0.00/5 (No votes)
8 Mar 2011 4  
Handling 301 Redirects in ASP.NET form basic page control to HHTP modules

Introduction

You are putting a new site together and you have lots of new pages and lots of old pages that will no longer exist, it might be a good idea to tell links from external sources and search engines, robots, etc... that the pages have moved. Supplying incoming requests with some notification of page moves or page replacements is handled with "301 Moved Permanently".

There are a few instances where this is a good idea:

  • Complete redesign of a site
  • Single Page move
  • Move from static site htm to aspx
  • You don't want to lose search rankings
  • You can't define all your external links

Background : What a "301 Moved Permanently" is

w3.org states: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

10.3.2 301 Moved Permanently

The requested resource has been assigned a new permanent URI and any future references to this resource SHOULD use one of the returned URIs. Clients with link editing capabilities ought to automatically re-link references to the Request-URI to one or more of the new references returned by the server, where possible. This response is cacheable unless indicated otherwise.

The new permanent URI SHOULD be given by the Location field in the response. Unless the request method was HEAD, the entity of the response SHOULD contain a short hypertext note with a hyperlink to the new URI(s).

If the 301 status code is received in response to a request other than GET or HEAD, the user agent MUST NOT automatically redirect the request unless it can be confirmed by the user, since this might change the conditions under which the request was issued.

Basic Redirects

If an old page still exists, then you can add code to the Page_Load event which will look like the code below:

protected void Page_Load(object sender, EventArgs e)
        {
            // Flag Page has moved
            Response.Status = "301 Moved Permanently";
            // Ask Browser to move to new page
            Response.AddHeader("Location", "NewPage_Redirect1.aspx");
        }

The above code has advantages and disadvantages:

Advantages

  • Quick
  • Easy

Disadvantages

  • You need to maintain that page from now on in your application
  • Does not work with downloadable content such as picture, PDFs, media, etc..
  • You still have to contain old folder structure in your new site.
  • Does not work with directories/folders redirects.
  • Does not work if moving site from Java, Apache to IIS

Expanding from Basic Redirects

So we need something that will give us a clean site and be able to handle a mix of incoming requests and move them to the right page or folder. Also dealing with non web application files such as pdf and Word documents would be good as well.

The best way to achieve this is to use a custom HTTP Module which will deal with requests and point the client browser to the correct page.

Creating a Custom HHTP Module for Redirect

First, you needed to create a class that inherits from the interface IHttpModule and implement two members as shown below.

Note: We will fill in the members later on

 public class HttpRedirect : IHttpModule
    {
        public void Init(HttpApplication context)
        {
            throw new NotImplementedException();
        }

        public void Dispose()
        {
            throw new NotImplementedException();
        }
    }

Create Entry in Web.Config for the Redirect Class

Below is an entry for the Redirect classes called "HttpRedirect" which will fire on an incoming request.

<modules>
        <add name="Redirects" type="Redirects301.HttpRedirect,Redirects301"/>
        <add name="ScriptModule" preCondition="managedHandler" 
        type="System.Web.Handlers.ScriptModule, System.Web.Extensions, 
        Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
</modules>

Redirect List

I had about 100 or so redirects and some might change or others might be added later so I added them in an XML file that I could load and changes if required without rebuilding the application.

So there is an XML file called  Redirect301.xml which holds the redirects.

<?xml version="1.0" encoding="utf-8" ?>
<Redirects>
  <Redirect>
    <Description>Change of Index</Description>
    <OldPage>/index.htm</OldPage>
    <NewPage>/index.aspx</NewPage>
    <File>FALSE</File>
  </Redirect>
  <Redirect>
    <Description>Directory </Description>
    <OldPage>/Profile</OldPage>
    <NewPage>/users</NewPage>
    <File>FALSE</File>
  </Redirect>
  <Redirect>
    <Description>PDF Redirect</Description>
    <OldPage>/lib/docs/TermsOfBusiness.pdf</OldPage>
    <NewPage>/docs/TOB.pdf</NewPage>
    <File>TRUE</File>
  </Redirect>
</Redirects>

The contents of this have the following function:

  • Description: Does nothing in Code, purely for describing the Redirect
  • OldPage: What the old page was, this will be the incoming request that you will look out for
  • NewPage: Target Location for the OldPage request.
  • File: The NewPage is a file download rather than a support format in IIS; I used this for PDFs

Adding Location of this to Web.Config

Simple Application Key as shown below, this is used by the HttpRedirect class to find the redirect XML.

      <appSettings>
           <add key="RedirectXMLDoc" value="/Redirects301.xml"/>
  </appSettings>

Redirect Class

Used to store the redirects in a usable format for the redirect. This roughly matches the contents of the Redirect301.xml file.

namespace Redirects301
{
        public class Redirect
        {
            public string OldPage { get; set; }
            public string NewPage { get; set; }
            public bool TransferFileType { get; set; }
        }
}

Business End: HttpRedirect Class

This is the full list of the HttpRedirect class.

using System;
using System.Collections.Generic;
using System.Web;
using System.Linq;
using System.Web.Configuration;
using System.Xml.Linq;

namespace letsure
{
    public class HttpRedirect : IHttpModule
    {
        public void Init(HttpApplication ap)
        {
            ap.BeginRequest += new EventHandler(ap_BeginRequest);

          }
        /// <summary>
        /// Filename of the Redirect301 xml document
        /// </summary>
        static string Xmldoc
        {
            get
            {
                string filename = WebConfigurationManager.AppSettings["RedirectXMLDoc"];
                return HttpContext.Current.Server.MapPath(filename);
            }
        }

        static void ap_BeginRequest(object sender, EventArgs e)
        {
                // Get current HttpApplication Request
                HttpApplication hxa = (HttpApplication) sender;


                //Get Incoming Request for page.
                string incomingText = hxa.Context.Request.Url.AbsolutePath.Trim();

                // This deals with ISS issues regarding it does not 
                // service non-existant pages
                // so create a custom 404.aspx page change ISS to attach 
                // custom 404 to this page
                // and this code will handle this.
                if (hxa.Request.Url.LocalPath.Contains("404.aspx"))
                {
                    incomingText = hxa.Request.Url.Query.ToLower();
                    //script away the errorpath from query string to get the original
                    //requested url
                    incomingText = incomingText.Replace("?aspxerrorpath=", string.Empty);

                    // Fix for SERVER IIS6

                    incomingText = incomingText.Replace("?404;", string.Empty);
                    string schemeHost = hxa.Request.Url.Scheme + "://" 
					+ hxa.Request.Url.Host;
                    incomingText = incomingText.Replace(schemeHost, string.Empty);

                    // Fix for Ports numbers on ports that are not default
                    string portvalue = ":" + hxa.Request.Url.Port;
                    incomingText = incomingText.Replace( portvalue, "");
                }

                // Try and Check for redirect
                try
                {

                    // Check if in list
                    var results = from r in Redirects()
                                  where incomingText.ToLower() == r.OldPage.ToLower()
                                  select r;

                    //Redirect if required.
                    if (results.Count() > 0)
                    {
                        //Select First Redirect if we have multiple matches
                        Redirect r = results.First();

                        // Add Moved Permanently 301 to response
                        hxa.Response.Status = "301 Moved Permanently";

                        //Test the Response is an attachment type ie not hmtl, 
		      //aspx, .htm, html, etc....
                        if (r.TransferFileType)
                        {
                            hxa.Response.AddHeader("content-disposition", 
				"attachment; filename=filename");
                        }

                        // Go to new page
                        hxa.Response.AddHeader("Location", r.NewPage);
                        //hxa.Response.End();
                    }
                }

                catch (Exception)
                {
                    //Redirect to page not found if we have an error
                    //hxa.Response.AddHeader("Location", @"\404.aspx");
                    throw;
                }
        }

        /// <summary>
        /// Dispose; there is nothing to dispose.
        /// </summary>
        public void Dispose() { }


        /// <summary>
        /// Return a collection of Redirect
        /// </summary>
        /// <returns></returns>
        private static IEnumerable<Redirect> Redirects()
        {
            //Get Collection of Redirects
            var results = new List<Redirect>();

            //Get Redirect File XML name for
            XDocument xdocument = GetXmLdocument();

            var redirects = from redirect in xdocument.Descendants("Redirect")
                            select new
                                       {
                                           oldpage = redirect.Element("OldPage").Value,
                                           newpage = redirect.Element("NewPage").Value,
                                           file = redirect.Element("File").Value
                                       };
            foreach (var r in  redirects)
            {
                var redirect = new Redirect
                                   {
                                       NewPage = r.newpage,
                                       OldPage = r.oldpage,
                                       TransferFileType = Convert.ToBoolean(r.file)
                                   };

                results.Add(redirect);
            }

            return results;
        }

        /// <summary>
        /// Get Redirect Doc from Cache
        /// </summary>
        /// <returns></returns>
        private static XDocument GetXmLdocument()
        {
            //create empty return doc
            XDocument xmldocument;

            //check if Cache is null; if so add it
            if (HttpRuntime.Cache["RedirectXMLDoc"] == null)
            {
                xmldocument = XDocument.Load(Xmldoc);
                HttpRuntime.Cache["RedirectXMLDoc"] = xmldocument;
            }
            else
            {
                //Get from Cache.
                xmldocument = (XDocument) HttpRuntime.Cache["RedirectXMLDoc"];
            }

            return xmldocument;
        }
    }
}
<appSettings>  </appSettings>

Points of Interest

Redirecting to a file supplied from the code below as it added a head to tell the client browser to expect a file. I could have tested the extension of the incoming request and decided if it was file or note, but adding something into the HTML is simpler and gives a little more control.

 //Test the Response is an attachment type ie not hmtl, aspx, .htm, html, etc....
 if (r.TransferFileType)
  {
     hxa.Response.AddHeader("content-disposition", "attachment; filename=filename");
  }

Issues

Some of you may have noticed a possible performance issue?

The issue is that for every request the Redirect XML document will have to be opened, read and searched. This is not ideal by any means and this should be cached as the application level and due to popular demand I have added this in.

Folder Redirection

This code does not redirect requested for folders; that is if you have a link for  /old/site.aspx and you want to go to /new/site.aspx. Placing a rule oldfile=/old newfile=/new will not redirect to page correctly. But if your incoming requests is /old, it will redirect to /new on a folder level as it will pickup your default page for that folder.

Testing

Test, Test again and then get someone else to Test again. Don't assume my code or anyone else's works the way it is supposed to, this worked for me but you might have problems.

Use

I used this code on the following without any issues with:

  • VS 2008
  • .NET FrameWork 3.5
  • IIS 6-7

Downloads

I don't think you need any, unless you can't copy or paste.

History

  • Version 1.000.001 17-Sept-2010
    • Initial release
  • Version 1.000.002 04-Mar-2011
    • Cache added
    • Fixes for IIS 5/6/7

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here