I mentioned a few days ago that I was working on a Web application that's dealing with custom extensions to route requests to a very large number of custom 'script' pages. Basically I have a custom IHttpHandler based base class and custom subclasses that implement the actual handler logic which is essentially a script like implementation to call business objects and return an XML response to the client.
I love using HttpHandlers in general because of the high performance and simple implementation model - they are just awesome for custom architectures that need to manage Web requests in very specific ways - it's been a perfect fit for this project which is a distributed Flash application with an XML based data backend.
One of the issues I've been fighting with is that we use custom extensions for our requests (and this had to be done for backwards compatibility reasons). Athough I can map an HttpHandler to the the SimpleHandlerFactory in web.config:
<httpHandlers>
<add path="*.xrns" verb="*" type="System.Web.UI.SimpleHandlerFactory" validate="false"/>
</httpHandlers>
which is all it takes to get things to execute just fine, the results in Visual Studio are less than optimal. As mentioned in my last post one issue with this scenario is that Visual Studio doesn't display this combination very nicely:
which is actually problem in this project as we'll be ending up with close to 100 script requests.
There are a couple of solutions I came up to get around this issue:
- Using .ashx in the project and rewriting the Url
- Creating a class factory HttpHandlerFactory
Rewriting Urls from .ashx to .xrns
The former is pretty easy to do - a quick module implementation can simply rewrite URLs ending in .xrns and route them to the appropriate .ashx scripts. Scott Guthrie recently posted a nice summary of Rewriting options for ASP.NET that point out ways that this can be done the best of which are a couple of module that you can use to use RegEx expressions to re-write Urls by way of web.config entires.
If the needs are simple though it's just as easy to create a quick module of your own and add it to your project so there's no need for external dependencies. Here's what I used:
public class xrnsToashxMappingModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.BeginRequest += new EventHandler(context_BeginRequest);
}
void context_BeginRequest(object sender, EventArgs e)
{
HttpContext Context = HttpContext.Current;
string Url = Context.Request.RawUrl;
string LowerUrl = Url.ToLower();
// *** ScriptExtension: ".xrns"
if ( !LowerUrl.Contains(App.Configuration.ScriptExtension) )
return;
Url = Regex.Replace(Url, App.Configuration.ScriptExtension, ".ashx",
RegexOptions.IgnoreCase);
Context.RewritePath(Url);
}
public void Dispose() { }
}
It's hooked up in web.config like this:
<httpModules>
<add name="xrnsToashxMappingHandler" type="SummaLPManager.xrnsToashxMappingModule,SummaLPManager" />
<add name="wwScriptCompression" type="Westwind.Web.Controls.wwScriptCompressionModule,westwind.web.controls" />
</httpModules>
The module basically looks for any .xrns extension entries and routes them to .ashx. So now in our project I can simply create and store .ashx handler files while inbound requests actually fire .xrns requests.
The reason .ashx is important is because I created a custom Item Template for these new handlers and by using ashx as the extension I can create both .ashx and .ashx.cs template files and Visual Studio will properly drop these files as associated files into the project. It will also allow double clicking on the .ashx file and open the source file automatically.
Direct class Routing: HttpHandlerFactory to a class
But then I got to thinking: Why bother with a .ashx file at all? Why not have the request automatically instantiate the requested class based on the filename of the inbound request? This would allow instantiation of the class purely with a .cs file in the project.
This can be accomplished with an HttpHandlerFactory which is an abstraction layer that sits in front of the handler creation. In Web.Config you can specify a handler hookup either by IHttpHandler or by IHttpHandlerFactory. A factory basically allows you to make decisions about which class to instantiate as a handler in the GetHandler() method.
So in my scenario I want to be able to look at the filename and based on that create instantiate the matching handler that has the same classname. Here's the code I used to do this:
/// <summary>
/// Handler Factory that allows executing classes that match a filename only
/// and dynamically instantiates a matching class.
///
/// This version specifically checks for the ISummaLPHandler interface on
/// the instantiated handler to avoid executing malicious code.
///
/// Use this handler factory only if you don't want to map to phsical
/// .ashx files. In the SummaLP system mapping .xrns to the SimpleHandlerFactory
/// works if .ashx files are to be used.
/// </summary>
public class xrnsHandlerFactory : IHttpHandlerFactory
{
/// <summary>
/// Internal static hashtable that caches constructors for dynamic activation
/// </summary>
private static Hashtable constructorList = new Hashtable();
private static object SyncLock = 0;
public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated)
{
string Filename = Path.GetFileNameWithoutExtension(url).ToLower();
Type PageType = null;
ConstructorInfo constructor = null;
if (!constructorList.ContainsKey(Filename))
{
lock(SyncLock)
{
if (!constructorList.ContainsKey(Filename))
{
constructor = constructorList[Filename] as ConstructorInfo;
Type[] types = Assembly.GetExecutingAssembly().GetTypes();
foreach (Type type in types)
{
if (type.Name.ToLower() == Filename)
PageType = type;
}
if (PageType == null)
{
context.Response.StatusCode = 404;
context.ApplicationInstance.CompleteRequest();
return null;
}
constructor = PageType.GetConstructor(new Type[0]);
constructorList.Add(Filename, constructor);
}
}
}
constructor = constructorList[Filename] as ConstructorInfo;
IHttpHandler Handler = constructor.Invoke(null) as IHttpHandler;
// *** ensure that we have a valid handler that matches our
// execution template classes
if (Handler is ISummaLPHttpHandler)
return Handler;
// *** Invalid Handler or couldn't load - return a 404 file not found
context.Response.StatusCode = 404;
return null;
}
public void ReleaseHandler(IHttpHandler handler)
{
}
}
which is hooked up in web.config like this:
<httpHandlers>
<add verb="*" path="*.xrns" type="SummaLPManager.xrnsHandlerFactory,SummaLPManager" />
</httpHandlers>
The code is a bit lengthy because it does some caching of the constructors to improve activation speed of the handlers. The code first picks up the filename of the request and then finds a matching class to instantiate. Note that I have to loop through the assembly types to do this since there's no easy way to get just the simple classname through the Assembly interfaces. Once a constructor is found it's cached in a list for fast access.
The constructor is then invoked to create an instance, the instance is checked for a specific interface, and if it doesn't match the request is refused, otherwise the handler is returned.
This works great and allows simply storing .cs files in the project.
In the end...
In the end after a bunch of back and forth with the customer we decided on using the UrlRewriting method and actually keeping the .ashx files in the project, because the .ashx files make it very obvious what type of object you are dealing with. As I mentioned this project would end up with about 100 different script files as well as a large number of fair number of classes and the easier it is to see what the purpose of each file is the better.
Other Posts you might also like