Redirect Routes and other Fun With Routing And Lambdas

asp.net mvc, asp.net 0 comments suggest edit

ASP.NET Routing is useful in many situations beyond ASP.NET MVC. For example, I often need to run a tiny bit of custom code in response to a specific request URL. Let’s look at how routing can help.

First, let’s do a quick review. When you define a route, you specify an instance of IRouteHandler associated with the route which is given a chance to figure out what to do when that route matches the request. This step is typically hidden from most developers who use the various extension methods such as MapRoute.

Once the route handler makes that decision, it returns an instance of IHttpHandler which ultimately processes the request.

There are many situations in which I just have a tiny bit of code I want to run and I’d rather not have to create both a custom route handler and http handler class.

So I started experimenting with some base handlers that would allow me to pass around delegates.

Warning: the code you are about to see may cause permanent blindness. This was done for fun and I’m not advocating you use the code as-is.

It works, but the readability of all these lambda expressions may cause confusion and insanity.

One common case I need to handle is permanently redirecting a set of URLs to another set of URLs. I wrote a simple extension method of RouteCollection to do just that. In my Global.asax.cs file I can write this:

routes.RedirectPermanently("home/{foo}.aspx", "~/home/{foo}");

This will permanently redirect all requests within the home directory containing the .aspx extension to the same URL without the extension. That’ll come in pretty handy for my blog in the future.

When you define a route, you have to specify an instance of IRouteHandler which handles matching requests. That instance in turn returns an IHttpHandler which ultimately handles the request. Let’s look at the simple implementation of this RedirectPermanently method.

public static Route RedirectPermanently(this RouteCollection routes, 
    string url, string targetUrl) {
  Route route = new Route(url, new RedirectRouteHandler(targetUrl, true));
  routes.Add(route);
  return route;
}

Notice that we simply create a route using the RedirectRouteHandler. As we’ll see, I didn’t even need to do this, but it helped make the code a bit more readable. To make heads or tails of this, we need to look at that handler. Warning: Some funky looking code ahead!

public class RedirectRouteHandler : DelegateRouteHandler {
  public RedirectRouteHandler(string targetUrl) : this(targetUrl, false) { 
  }

  public RedirectRouteHandler(string targetUrl, bool permanently)
    : base(requestContext => {
      if (targetUrl.StartsWith("~/")) {
        string virtualPath = targetUrl.Substring(2);
        Route route = new Route(virtualPath, null);
        var vpd = route.GetVirtualPath(requestContext, 
          requestContext.RouteData.Values);
        if (vpd != null) {
          targetUrl = "~/" + vpd.VirtualPath;
        }
      }
      return new DelegateHttpHandler(httpContext => 
        Redirect(httpContext, targetUrl, permanently), false);
    }) {
    TargetUrl = targetUrl;
  }

  private void Redirect(HttpContext context, string url, bool permanent) {
    if (permanent) {
      context.Response.Status = "301 Moved Permanently";
      context.Response.StatusCode = 301;
      context.Response.AddHeader("Location", url);
    }
    else {
      context.Response.Redirect(url, false);
    }
  }

  public string TargetUrl { get; private set; }
}

I bolded a portion of the code within the constructor. The bolded portion is a delegate (via a lambda expression) being passed to the base constructor. Rather than creating a custom IHttpHandler class, I am telling the base class what code I would want that http handler (if I had written one) to execute in order to process the request. I pass that code as a lambda expression to the base class.

I think in a production system, I’d probably manually implement RedirectRouteHandler. But while I’m having fun, let’s take it further.

Let’s look at the DelegateRouteHandler to understand this better.

public class DelegateRouteHandler : IRouteHandler {
  public DelegateRouteHandler(Func<RequestContext, IHttpHandler> action) {
    HttpHandlerAction = action;
  }

  public Func<RequestContext, IHttpHandler> HttpHandlerAction {
    get;
    private set;
  }

  public IHttpHandler GetHttpHandler(RequestContext requestContext) {
    var action = HttpHandlerAction;
    if (action == null) {
      throw new InvalidOperationException("No action specified");
    }
    
    return action(requestContext);
  }
}

Notice that the first constructor argument is a Func<RequestContext, IHttpHandler>. This is effectively the contract for an IRouteHandler. This alleviates the need to write new IRouteHandler classes, because we can instead just create an instance of DelegateRouteHandler directly passing in the code we would have put in the custom IRouteHandler implementation. Again, in this implementation, to make things clearer, I inherited from DelegateRouteHandler rather than instantiating it directly. To do that would have sent me to the 7^th^ level of Hades immediately. As it stands, I’m only on the 4^th^ level for what I’ve shown here.

The last thing we need to look at is the DelegateHttpHandler.

public class DelegateHttpHandler : IHttpHandler {

  public DelegateHttpHandler(Action<HttpContext> action, bool isReusable) {
    IsReusable = isReusable;
    HttpHandlerAction = action;
  }

  public bool IsReusable {
    get;
    private set;
  }

  public Action<HttpContext> HttpHandlerAction {
    get;
    private set;
  }

  public void ProcessRequest(HttpContext context) {
    var action = HttpHandlerAction;
    if (action != null) {
    action(context);
    }
  }
}

Again, you simply pass this class an Action<HttpContext> within the constructor which fulfills the contract for IHttpHandler.

The point of all this is to show that when you have interfaces with a single methods, they can often be transformed into a delegate by writing a helper implementation of that interface which accepts delegates that fulfill the same contract, or at least are close enough.

This would allow me to quickly hack up a new route handler without adding two new classes to my project. Of course, the syntax to do so is prohibitively dense that for the sake of those who have to read the code, I’d probably recommend just writing the new classes.

In any case, however you implement it, the Redirect and RedirectPermanent extension methods for handling routes is still quite useful.

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

11 responses

  1. Avatar for Erik
    Erik December 15th, 2008

    I did something similar as an experiment in creating an IList implementation which uses Action and Func properties to fulfill its contract. Posted what I came up with on my blog -- shadowcoding.blogspot.com/.../...d-collection.html

  2. Avatar for Erik
    Erik December 15th, 2008

    Sorry - meant 'ICollection implementation' instead of 'IList'

  3. Avatar for Bryan Watts
    Bryan Watts December 15th, 2008

    I like DelegateRouteHandler, but I immediately assumed it would be used like this:
    new DelegateRouteHandler(context => ...interesting things...);
    This allows the dynamic bit of logic to be specified, instead of having to override an abstract method (and create a class in the process).
    Deriving and passing in a function in the constructor seems like another way to say "abstract method", except you don't get the compiler-enforced existence of an implementation :-)

  4. Avatar for Erik
    Erik December 15th, 2008

    Bryan - that's how my delegate-based ICollection implementation works.

  5. Avatar for haacked
    haacked December 16th, 2008

    @Bryan you can actually use it that way. The reason I chose to implement another class was to use the DelegateRouteHandler directly, you need to pass a lambda, which itself takes in a lambda.
    Nested lambdas are hard to format in such a way that your head doesn't spin in circles. :)

  6. Avatar for Bryan Watts
    Bryan Watts December 16th, 2008

    I guess I didn't expect to see a specific derived class once you had a class which can handle all implementations (very functional of you!)
    RedirectPermanently could use DelegateRouteHandler directly and wrap a private method which does the same work, avoiding the constructor codejitsu (and an artifact):

    Route route = new Route(url, new DelegateRouteHandler(context => GetRedirectHandler(context, url, true)));
    private static IHttpHandler GetRedirectHandler(RequestContext context, string targetUrl, bool permanently)
    {
    // The body of the constructor function
    }

    > Nested lambdas are hard to format in such a way that your head doesn't spin in circles. :)
    We'll need an old priest and a young priest.

  7. Avatar for haacked
    haacked December 16th, 2008

    @Bryan what you did is exactly what I should have done. The lead dev on ASP.NET MVC told me to do something similar at lunch today. :)

  8. Avatar for Bryan Watts
    Bryan Watts December 16th, 2008

    It's the poor-man's anonymous interface implementation. I've used it quite a bit with functional constructs in C#, especially Linq-related.
    It seems to fit quite nicely into ASP.NET MVC.

  9. Avatar for Javier Lozano
    Javier Lozano December 17th, 2008

    Nice post, Phil!
    I think one thing you've uncovered in this post is to make a clear distinction that Routing is something that the MVC framework uses, rather than being tightly coupled.
    Another cool thing about the post is to show the flexibility of code that uses Func<T,R> as an implementation of a command pattern.

  10. Avatar for Rodrigo
    Rodrigo December 28th, 2008

    Phil,
    I've seen that in the next release there will be no code-behind, which is great, but how to handle strongly typed view data?
    Following Scott Gu, I could use ViewData<T> in the code-behind and then have something like this in my views...
    Product.Name
    How will that be handled in this new scenario?

  11. Avatar for G&#252;ltekin KAYA
    G&#252;ltekin KAYA November 30th, 2011

    I'm trying to do the same for my web forms 4 application. May you forward me to the right direction. If I'm not mistaken this seems like it is for mvc so how may I manage to do the same thing in web forms application?
    Thank you.