views:

57

answers:

1

I'm trying to find a good general purpose way to canonicalize urls in an ASP.NET MVC 2 application. Here's what I've come up with so far:

// Using an authorization filter because it is executed earlier than other filters
public class CanonicalizeAttribute : AuthorizeAttribute
{
    public bool ForceLowerCase { get;set; }

    public CanonicalizeAttribute()
        : base()
    {
        ForceLowerCase = true;
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        RouteValueDictionary values = ExtractRouteValues(filterContext);
        string canonicalUrl = new UrlHelper(filterContext.RequestContext).RouteUrl(values);
        if (ForceLowerCase)
            canonicalUrl = canonicalUrl.ToLower();

        if (filterContext.HttpContext.Request.Url.PathAndQuery != canonicalUrl)
            filterContext.Result = new PermanentRedirectResult(canonicalUrl);
    }

    private static RouteValueDictionary ExtractRouteValues(AuthorizationContext filterContext)
    {
        var values = filterContext.RouteData.Values.Union(filterContext.RouteData.DataTokens).ToDictionary(x => x.Key, x => x.Value);
        var queryString = filterContext.HttpContext.Request.QueryString;
        foreach (string key in queryString.Keys)
        {
            if (!values.ContainsKey(key))
                values.Add(key, queryString[key]);
        }
        return new RouteValueDictionary(values);
    }
}

// Redirect result that uses permanent (301) redirect
public class PermanentRedirectResult : RedirectResult
{
    public PermanentRedirectResult(string url) : base(url) { }

    public override void ExecuteResult(ControllerContext context)
    {
        context.HttpContext.Response.RedirectPermanent(this.Url);
    }
}

Now I can mark up my controllers like this:

[Canonicalize]
public class HomeController : Controller { /* ... */ }

This all appears to work fairly well, but I have the following concerns:

  1. I still have to add the CanonicalizeAttribute to every controller (or action method) I want canonicalized, when it's hard to think of a situation where I won't want this behaviour. It seems like there should be a way to get this behaviour site-wide, rather than one controller at a time.

  2. The fact that I'm implementing the 'force to lower-case' rule in the filter seems wrong. Surely it would be better to somehow role this up into the route url logic, but I can't think of a way to do this in my routing configuration. I thought of adding @"[a-z]*" constraints to the controller and action parameters (as well as any other string route parameters), but I think this will cause the routes to not be matched. Also, because the lower-case rule isn't being applied at the route level, it's possible to generate links in my pages that have upper-case letters in them, which seems pretty bad.

Is there something obvious I'm overlooking here?

+2  A: 

You could write a custom route.

Darin Dimitrov
+1. This will do it, but I was hoping there'd be a cleaner way of doing it than handling the Application_BeginRequest event. I'll mark this as the answer if no-one comes up with anything better by tomorrow. Thanks.
Bennor McCarthy
Actually, I don't think handling Application_BeginRequest is the best place to do it: I don't think I'd have access to RequestContext or RouteData, so I can't use the routing configuration to verify my url is correct. I can easily check the url is lower-case, but not that it exactly matches what my routes would generate.
Bennor McCarthy
@Bennor you can create a `RequestContext` given the native `HttpContext` object you have: `HttpContextBase httpContext = new HttpContextWrapper(context); var routes = RouteTable.Routes.GetRouteData(httpContext); RequestContext requestContext = new RequestContext(httpContext, routes);`
Darin Dimitrov
Excellent. Guess I should have done my research before I opened my mouth on that... :)
Bennor McCarthy
As an aside, do you think it's really necessary to do it on Application_BeginRequest, or would Session_Start be enough, as my understanding is that it's only really an issue for inbound links?
Bennor McCarthy