Insights and discoveries
from deep in the weeds
Outsharked

Thursday, March 29, 2012

Passing control from a custom HttpHandler to the default handler in asp.net

Using System.Web.Routing and IIS7 you can do some pretty interesting stuff with a web app. It also opens up a lot of possibilities for "modernizing" old webforms applications that are stuck with ugly paths and query strings by overriding the default handlers and parsing out the page path yourself.

One thing I wanted to do was to map certain paths back to an aspx page, but dynamically - e.g. I wanted to be able to parse out a path and using complex logic, build a new "real" path+querystring that I could just pass to the default handler. This way I can create nice clean API-like paths and map them to an old, ugly query-string based API. The routing part is a piece of cake. (Well, not really, but it's not a mystery). But how do you invoke the default handler from code? There's a baffling lack of info out there, so I figured I'd post a solution for future coders.

After some digging I realized that, actually, the default handler is just the plain old System.Web.UI.Page object. I tried to just create an instance of one and call ProcessRequest like you would any other handler. Nothing at all. No error, no output.

Microsoft is decidedly no help with this either. For Page.ProcessRequest, their documentation actually says:

You should not call this method.
Priceless!

Let us proceed to tread into "you have been warned" territory, though. The problem is you (apparently) aren't just supposed to create a new Page. You should use the PageHandlerFactory. Unfortunately, MS has inconveniently laden that thing with an internal constructor, so you can't make one of those, either.

Thanks to Robert's C# musings for the answer to this one, which is the key to solving this problem. You can't instantiate a class with an internal constructor, directly. But you can make yourself an instance of any class that doesn't call any constructor using a well-hidden GetUninitializedObject method. Since the constructor in the case of this factory is designed only to prevent us from using the factory, and doesn't actually do antyhing useful, not a problem. Once you've got access to the handler factory, the rest falls into place pretty easily. Here's some basic code that maps any path to default.aspx, converting the original path to a query parameter.

public void ProcessRequest(HttpContext context) {

    // the internal constructor doesn't do anything but prevent you from instantiating
    // the factory, so we can skip it.
    
    PageHandlerFactory factory =
        (PageHandlerFactory)System.Runtime.Serialization.FormatterServices
            .GetUninitializedObject(typeof(System.Web.UI.PageHandlerFactory));

    // you may want to use context.PathInfo - in my case I mapped a wildcard to this
    // handler so it's always blank.

     string newTarget  = "default.aspx"; 
     string newQueryString = "path="+context.Path;
     string oldQueryString = context.Request.QueryString.ToString();
     string queryString = newQueryString + oldQueryString!="" ? 
         "&" + newQueryString :
         "";

     // the 3rd parameter must be just the path to the file target (no querystring).
     // the 4th parameter should be the physical path to the file, though it also
     //   works fine if you pass an empty string - perhaps that's only to override
     //   the usual presentation based on the path?

     var handler = factory.GetHandler(context, "GET",newTarget,
         context.Request.MapPath(context,newTarget));

     // Update the context object as it should appear to your page/app, and
     // assign your new handler.

     context.RewritePath(newTarget, "", queryString);
     context.Handler = handler;

     // .. and done

     handler.ProcessRequest(context);
}

The PageHandlerFactory can certainly be created statically so you don't have the overhead of reflection for every request. The actual "Page" handler must be created each time, though, because it's not marked as IsReusable.

No comments:

Post a Comment