Thursday, April 21, 2005

Make your own webhandler factory, why? Because you can!

In ASP.Net, the framework provide a default handler for ashx files. It is called
"System.Web.UI.SimpleHandlerFactory". You can find the mapping in machine.config.
The main job of this handler is to compile an ashx file, find the class that implements IHttpHandler, instanciate it and call its Process request so your handler can do its job.
This "System.Web.UI.SimpleHandlerFactory" does all this for you. I wanted to make my own, just for fun... Actually for my students who want to know a little more of what is going on behind the scene.

There are 3 parts to this:
- 1: A standard ashx file to make sure my new factory can create my handler and pass the request through.
- 2: A factory class that needs to be compiled as a library and placed in the bin directory. The name of the assembly needs to be webhandler.dll for this demo.
- 3: A web.config to remap the standard "System.Web.UI.SimpleHandlerFactory" to "DM.SimpleWebHandlerFactory"

Enjoy!

Part 1: Here is a standard ashx file


<%@ WebHandler Language="C#" class="SimplestHandlerEver" %>

using System;
using System.Web;

class SimplestHandlerEver : IHttpHandler
{
public SimplestHandlerEver()
{
}

public void ProcessRequest(HttpContext context)
{
context.Response.Write(this.GetType().FullName + " called");
}

public bool IsReusable
{
get
{
return false;
}
}
}
//--- End of File

Part 2: Here is the custom factory that you need to compile and place in the bin directory.

using System;
using System.Text;
using System.Web;
using Microsoft.CSharp;

namespace DM
{
///
/// Author: C. Fouquet
/// This is a very simple handler factory that can be used to replace the
/// standard System.Web.UI.SimpleHandlerFactory that is mapped to
/// the .ashx extension in the machine.config
/// Needless to say, this is a very crude implementation of an IHttpHandlerFactory.
/// This is just to illustrate the rough steps the actual System.Web.UI.SimpleHandlerFactory
/// goes through to create the handler.
/// It is very inefficient. The code is compiled every time, the generated assembly is
/// loaded every time.
/// Also, look at the web.config to see how we remap the default handler factory
///

public class SimpleWebHandlerFactory : IHttpHandlerFactory
{

public SimpleWebHandlerFactory()
{

}
#region IHttpHandlerFactory Members

public void ReleaseHandler(IHttpHandler handler)
{

}


public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated)
{
string HandlerRequested = pathTranslated;
string Language ;
string Class;

// Open the .ashx file being hit
using(System.IO.StreamReader fs = System.IO.File.OpenText(HandlerRequested))
{

// The first line is the webhandler directive. Or so we hope!
string Directive = fs.ReadLine();

// Extract the information we are looking for from the directive
if(ProcessDirective(Directive, out Language, out Class))
{
if(Language.ToUpper() != "C#" && Language.ToUpper()!="CSHARP")
{
throw new Exception(Language + " Language Not Supported");
}
}
else
{
throw new Exception("missing or badly formatted webhandler directive");
}

string TheCode = fs.ReadToEnd(); // This is the rest of the code

string Target = System.IO.Path.GetTempFileName(); // We need an output
// file name so let the OS pick it.

// For simplicity here, we just include what we need. Obviously
// we would have to process the import directives but .. not today
// This is just a demo remember?
string[] Imports = new string[] {
"System.Web.dll"
};

System.Collections.Specialized.ListDictionary options = new System.Collections.Specialized.ListDictionary();
options.Add("target","library"); // equivalent of doing csc /t:library ...

CompilerError[] Errors = Compiler.Compile(
new string[] { TheCode },
new string[] { "TheCode" },
Target,
Imports,
options);

// Now we can load it
System.Reflection.Assembly HandlerAssembly = System.Reflection.Assembly.LoadFile(Target);

// And find the type we listed in the directive
IHttpHandler TheActualHandler = (IHttpHandler) HandlerAssembly.CreateInstance(Class);

return TheActualHandler;
}
}


// This parses the Directive. It is a little heavy on regex.. but regex are so much fun!

private bool ProcessDirective(string Directive, out string Language, out string Class)
{
Language = Class = string.Empty;

string BackSlash ="\\";
string DoubleQuote ="\"";

StringBuilder sb = new StringBuilder();
sb.Append("<%@");
sb.Append(BackSlash);sb.Append("s+");
sb.Append("WebHandler");
sb.Append(BackSlash);sb.Append("s+");


// Language="C#"
sb.Append("Language=");sb.Append(DoubleQuote);
sb.Append("(?[A-Za-z0-9]");sb.Append(BackSlash);sb.Append("#?)");
sb.Append(DoubleQuote);

// Any space
sb.Append(BackSlash);sb.Append("s+");

// class="xyx"
sb.Append("class=");sb.Append(DoubleQuote);
sb.Append("(?");sb.Append(BackSlash);sb.Append("w+)");
sb.Append(DoubleQuote);

// Any space
sb.Append(BackSlash);sb.Append("s+");

sb.Append("%<");

string regex = sb.ToString();

System.Text.RegularExpressions.RegexOptions options = ((System.Text.RegularExpressions.RegexOptions.IgnorePatternWhitespace | System.Text.RegularExpressions.RegexOptions.Singleline)
| System.Text.RegularExpressions.RegexOptions.IgnoreCase);
System.Text.RegularExpressions.Regex reg = new System.Text.RegularExpressions.Regex(regex, options);

System.Text.RegularExpressions.Match m = reg.Match(Directive);
if(m.Success)
{
Language = m.Groups["Language"].Value;
Class = m.Groups["Class"].Value;
return true;
}

return false;

}


#endregion
}
}

Part 3: The web.config

<configuration>
<system.web>
<httpHandlers>
<remove verb="*" path="*.ashx"/>
<add verb="*" path="*.ashx" type="DM.SimpleWebHandlerFactory, webhandler"/>
</httpHandlers>
</system.web>
</configuration>

No comments: