Table of contents
Introduction
This article offers a simple solution to mapping URL requests within HttpHandler
(or HttpModule
, if needs to be) to the form of {controller}/{action}
,
much like with MVC Routing, but without using MVC.
The solution is based on a small and independent class SimpleRouter
,
optimized for work in a performance-demanding application.
Background
The mechanism of automatic URL routing is one of MVC's greatest virtues, allowing
developers to designate areas, controllers and actions to reflect logic of their
application in the most intuitive way, and not just internally, but on the protocol
(URL) level at the same time.
Once you have started using this approach, it is difficult to part with. However,
there are certain tasks where you may want to stay away from it, and I do not mean
the URL routing itself, but the entire MVC platform. Such tasks may include, but
not limited by:
- General low-level optimization tasks that require combination of quickest
response and smallest footprint;
- Intercepting or redirecting certain requests/parts of your website to a
high-performing assembly;
- Writing your own low-level server that requires utmost scalability.
Most of the time it would mean resorting to such interfaces as HttpHandler
or HttpModule
. There is even a lower protocol than this, class
HttpListener
, but on this level there is no automatic integration with
IIS, it only bundles well with self-hosting solutions.When using such class as
HttpHandler
(without any MVC), the first thing that's
missing greatly is the Automatic URL Routing. Without such useful thing
working, your project, as it grows, may end up being difficult to understand - which
piece of code corresponds to which request, and how they are interconnected.
What this article offers is to solve this basic problem. If you do not want or
can't use the MVC layer in your HttpHandler
, but want to keep the concise
controller/action architecture in your code, you can use this solution.
It is not by any means one-to-one with MVC Routing, it is not supposed to be.
Instead, this library focuses on benefits that are important when using class
HttpHandler
, such as:
- smallest footprint
- fastest execution
- quick and flexible integration
Specification
URL Routing can be a vast subject, and offers almost infinite possibilities
for implementation. This is why it is very important to set clear goals here before
we even begin.
Below is the exact list of goals that we set out to achieve in our version of
URL Routing.
Goals/Requirements
- Requests are accepted only in the form of
{controller}/{action}?[parameters]
- Processing for controllers, actions and action parameters is not case-sensitive.
- Controllers can reside in any namespace, and in any assembly.
- Only public classes and public non-static actions can map to a request.
- To address a controller class whose name ends with "Controller",
the latter can be omitted in the request.
- Parameters in the request are mapped to the corresponding action parameters
by their names (case-insensitive), while their order in the request is irrelevant.
- Actions support all standard parameter types that may be passed via URL:
- All simple types: string, bool, all integer and floating-point types;
- Arrays of specified simple types;
- Arrays of unspecified/mixed simple types;
- Parameters with default values;
- Parameters, declared as nullable.
- Application that configures
HttpHandler
as reusable can also
set the router to work with reusable controllers.
- Each controller is automatically offered direct access to the current HTTP
context.
- Optimized and well-organized implementation
- Only System.Web is used in combination with the Generics;
- Minimalistic - implemented in just one class;
- Fast-performing;
- Thoroughly documented.
Additional Provisions
- Default controllers and actions are not supported.
- Return type for an action is irrelevant and ignored.
- Prefix segments are not used, i.e. we use only the last two segments
in the request. For example, request www.server.com/one/two/three/controller/action
will only use controller/action, while prefix one/two/three
is not used, however made available to the controller/action, in
case it is needed.
Using the code
If you download the source code of the demo project, it is quite self-explaining. The solution includes three projects:
- BasicRouter - the core library with class
SimpleHandler
, which implements the routing.
- TestServer - a simple implementation of
HttpHandler
, plus a few demo controllers just to show how it all works.
- TestClient - a single-page web application as a client that makes requests into TestServer. It was created to simplify
testing the library, and to add some interesting benchmarking and statistics.
Adding the library
The recommended way of using this library is by adding project BasicRouter to your solution, as the library produces just a 12KB DLL. However,
it will work just as well within your own assembly.
Below is implementation of the TestServer dynamic library, which shows how to declare and initialize the use of the router.
HttpHandler Class
public class SimpleHandler : IHttpHandler
{
private static SimpleRouter router = null;
#region IHttpHandler Members
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext ctx)
{
if (!router.InvokeAction(ctx)) router.InvokeAction(ctx, "error", "details"); }
#endregion
public SimpleHandler()
{
if (router == null)
{
router = new SimpleRouter(IsReusable);
router.AddNamespace("TestServer.Controllers");
router.OnActionException += new SimpleRouter.ActionExceptionHandler(OnActionException);
}
}
private void OnActionException(HttpContext ctx, string action, Exception ex)
{
Exception e = ex.InnerException ?? ex;
StackFrame frame = new StackTrace(e, true).GetFrame(0);
string source, fileName = frame.GetFileName();
if(fileName == null)
source = "Not Available";
else
source = String.Format("{0}, <b>Line {1}</b>", fileName, frame.GetFileLineNumber());
ctx.Response.Write(String.Format("<h3>Exception was raised while calling an action</h3><ul><li><b>Action:</b> {0}</li><li><b>Source:</b> {1}</li><li><b>Message:</b> <span style=\"color:Red;\">{2}</span></li></ul>", action, source, e.Message));
}
}
First off, it contains the object of type SimpleRouter
- the very
class that implements our routing:
private static SimpleRouter router = null;
The object is created and initialized inside the constructor:
- Creating the object, telling it to activate access to reusable controllers
based on how our handler is being set up;
- Registering all the namespaces where our controller classes reside;
- Optional: Registering a handler for any exception that an action may throw.
The most interesting part is method ProcessRequest
:
public void ProcessRequest(HttpContext ctx)
{
if (!router.InvokeAction(ctx)) router.InvokeAction(ctx, "error", "details"); }
The method first calls InvokeAction
to locate controller/action
that correspond to the request, and if found, invoke the action. If that fails,
it invokes action on a controller that we created to process errors.
Controllers
I created a few demo controllers in file Controllers.cs for the test
and to show that you are very flexible in how you want to define action parameters.
Those examples are shown below.
public class SimpleController : BaseController
{
public void Time()
{
Write(DateTime.Now.ToString("MMM dd, yyyy; HH:mm:ss.fff"));
}
public void Birthday(string name, int age)
{
Write(String.Format("<h1>Happy {0}, dear {1}! ;)</h1>", age, name));
}
public void Exception(string msg)
{
throw new Exception(msg);
}
public void Prefix()
{
string s = String.Format("{0} segments in the request prefix:<ol>", prefix.Length);
foreach (string p in prefix)
s += String.Format("<li>{0}</li>", p);
Write(s + "</ol>");
}
}
public class ListController : BaseController
{
public void Sum(int [] values)
{
int total = 0;
string s = "";
foreach (int i in values)
{
if (!string.IsNullOrEmpty(s))
s += " + ";
s += i.ToString();
total += i;
}
s += " = " + total.ToString();
Write(s);
}
public void Add(double [] values, string units = null)
{
double total = 0;
foreach (double d in values)
total += d;
Write(String.Format("Total: {0} {1}", total, units));
}
public void Text(string[] values, string color = "Green")
{
string result = String.Format("<p style=\"color:{0};\">", color);
foreach(string s in values)
result += s + "<br/>";
result += "</p>";
Write(result);
}
public void Any(object[] values, string desc = null)
{
string s = (desc ?? "") + "<ol>";
foreach (object obj in values)
s += "<li>" + obj.ToString() + "</li>";
Write(s + "</ol>");
}
}
public class ImageController : BaseController
{
public void Diagram()
{
if (image == null)
image = FileToByteArray(ctx.Server.MapPath("~/Routing.jpg"));
ctx.Response.ContentType = "image/jpeg";
ctx.Response.BinaryWrite(image);
}
private static byte[] FileToByteArray(string fileName)
{
FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read);
long nBytes = new FileInfo(fileName).Length;
return new BinaryReader(fs).ReadBytes((int)nBytes);
}
private byte[] image = null; }
public class ErrorController:BaseController
{
public void Details()
{
string path = GetQueryPath();
if (string.IsNullOrEmpty(path))
path = "<span style=\"color:Red;\">Empty</span>";
string msg = String.Format("<p>Failed to process request: <b>{0}</b></p>", path);
msg += "<p>Passed Parameters:";
if (ctx.Request.QueryString.Count > 0)
{
msg += "</p><ol>";
foreach (string s in ctx.Request.QueryString)
msg += String.Format("<li>{0} = {1}</li>", s, ctx.Request.QueryString[s]);
msg += "</ol>";
}
else
msg += " <b>None</b></p>";
Write(msg);
}
}
In addition, it is worthwhile mentioning that according to requirement 4, routing
was implemented in such a way as to use only public classes and only public, non-static
actions.
Testing the code
The simplest way to see the code working is by running the demo application that's attached to this article.
If you want to set it up on your local IIS, but not sure how to do that, the steps below will help you.
- Either build the demo project or unpack its binary version.
- In IIS7, add a new website or new application, specifying the physical
path to the project's folder (where Web.config sits). Also, tell it
to use ASP.NET v4.0 as the application pool.
- Select your website, and from context menu->Manage
Web Site->Browse, to see that it opens. The default page should
open with our error image, which is by design, because we do not support unspecified
controller/action.
- Launch Powershell in Administrative mode, and type in there
inetsrv/appcmd list wp, to get Id-s of all application
pools currently running in IIS.
- In VS-2010, open the project and select menu Debug->Attach
To Process, make sure to switch Show processes in all sessions
ON. Find the process with that Id you saw in Powershell and click
Attach.
- Now, if you set a break-point and send a request from the browser, you will
be able to debug the code.
To simplify testing the code even further, I published it on one of my hosting
accounts, and though I understand it cannot be permanent, it will save some people
time testing it online, at least for the next few month, and perhaps I will give
it a more permanent hosting later and update the links here.
Try a few examples below, according to the controllers we have in the source
code:
-
/simple/time, outputs current date/time
-
/simple/birthday?name=John&age=25, outputs formatted result
-
/simple/exception?msg=some exception text, action throws an exception, and
the handler catches it
-
/one/two/three/four/five/simple/prefix, shows how prefix segments are taken
away
-
/list/sum?values=1,2,3,4,5, outputs sum of values (use of arrays)
-
/list/add?values=1.03,2.17&units=Dollars, outputs formatted values (use
of optional parameters)
-
/list/text?values=first,second,third, outputs array of strings
-
/list/any?values=one,2,-3.4&desc=mixed parameters, outputs array of
mixed-type parameters
- /image/diagram, outputs
an image
There are many other variations of requests that can be recognized by our demo controllers
- see it in the source code.
Custom Types
One way of supporting objects of custom types is by using type object[]
as an action parameter, like shown in the following example:
public void Any(object[] values, string desc = null)
{
string s = (desc ?? "") + "<ol>";
foreach (object obj in values)
s += "<li>" + obj.ToString() + "</li>";
Write(s + "</ol>");
}
If you know which custom type parameter values
represents, then
you can easily initialize its properties. In the above example though, we just write
all the passed values into the response.
This approach is also good for just passing an array of mixed data types as a
parameter.
URL Filters
In the attached demo I used the following configuration settings for the client:
<system.web>
<httpHandlers>
<add verb="*" path="data/*/*" type="TestServer.SimpleHandler, TestServer" />
</httpHandlers>
</system.web>
That's because the client is a UI application that also needs to return such files as
HTML, CSS, JS and images. So, in order to avoid dealing with those we used prefix
"/data" to filter them out from the requests for controller/action
.
This however, created a limitation for the UI demo itself, so it can only understand
requests that come as /data/controller/action
, and it cannot show use
of prefix segments, for instance.
You may ask, so what if I want to handle all the requests within my HttpHandler
,
and not just the ones for controller/action
? What does it take for
my HttpHandler
to have full control over the response?
The answer is - you need to be able to handle any request for a file and then
return the contents of such file, and avoid mixing it with controller/action
.
To show how it can be done in a simple scenario I included class HttpFileCache
within project TestServer. So let's make a few small changes in our demo
application to see how it works.
First off, we modify file Web.config in project TestClient to let our HttpHandler
catch all the incoming requests as shown below:
<system.web>
<httpHandlers>
<add verb="*" path="*" type="TestServer.SimpleHandler, TestServer" />
</httpHandlers>
</system.web>
Now we add support for handling files within our HttpHandler
class as below:
public class SimpleHandler : IHttpHandler
{
HttpFileCache cache = new HttpFileCache();
}
And then we change method ProcessRequest
:
public void ProcessRequest(HttpContext ctx)
{
if (cache.ProcessFileRequest(ctx) == ProcessFileResult.Sucess)
return;
if (!router.InvokeAction(ctx)) router.InvokeAction(ctx, "error", "details"); }
Now, if a request for a file comes in, it will be handled by class HttpFileCache
, and if it is not for an existing file,
then we try to map the request into controller/action
. This solution gives us full flexibility in handling the requests,
including use of prefix segments within a UI application or any web application that needs to return files along with
handling requests for controller/action
.
It is important to note that we used class HttpFileCache
mainly because the returned files must be cached, or it will
kill the performance. And class HttpFileCache
offers a very simple implementation for file caching, it does not allow
for processing requests for big files that may come in as uploads, for instance (see declaration of HttpFileCache
), i.e.
handlig big files requires a little extra work for returning partial contents.
Implementation
Class SimpleRouter
is well-documented, and has quite simple logic,
so I don't see it justified re-publishing the code here in full. I will just list
a few aspects of implementation that I found most interesting and/or challenging,
and as for the rest - have a look at file SimpleRouter.cs - it's all there,
and it is not much.
The way classes and their methods are located is quite generic, there is nothing
special there, just using methods Type.GetType
to find the right class,
and method Type.GetMethod
to find the action method.
When reusable controllers are active, I used a very simple cache implementation
to store controllers and pull them from there when needed again.
Perhaps the only complicated bit in the entire implementation was in preparing
array of parameters that need to be passed to an action. It is all done within the
method shown below:
private object[] PrepareActionParameters(ParameterInfo[] pi, NameValueCollection nvc)
{
List<object> parameters = new List<object>(); foreach (ParameterInfo p in pi)
{
object obj = nvc.Get(p.Name); if (string.IsNullOrEmpty((string)obj))
{
if (!p.IsOptional)
return null;
parameters.Add(p.DefaultValue); continue;
}
if (p.ParameterType != typeof(string))
{
try
{
if (p.ParameterType.IsArray)
{
string[] str = ((string)obj).Split(arraySeparator);
Type baseType = p.ParameterType.GetElementType();
Array arr = Array.CreateInstance(baseType, str.Length);
int idx = 0;
foreach (string s in str)
arr.SetValue(Convert.ChangeType(s, baseType), idx++);
obj = arr;
}
else
{
Type t = p.ParameterType;
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>)) t = Nullable.GetUnderlyingType(t);
obj = Convert.ChangeType(obj, t); }
}
catch (Exception)
{
return null; }
}
parameters.Add(obj);
}
return parameters.ToArray(); }
This one was a good exercise, trying to handle all the situations listed in the
requirements. It wasn't straightforward at all, figuring out how to properly initialize
values for parameters - arrays (I saw many posts from people getting stuck on this
one), and then handling nullable parameters.In the end, what matters, it is all
quite generic, and all the type conversion is done by method Convert.ChangeType
,
i.e. I am not dealing with any particular type here, and it is all quite elegant
that way.
Benchmarks and Conclusions
Such library can be benchmarked only on a local PC, because testing it on any
web host will solely reflect performance of the hosting server, and not of the library.
What we are interested in, primarily, is how long does it take on average between
request arriving into method ProcessRequest
and when the corresponding
action begins its execution on a cached controller. In other words, how long does
it take to route your average URL request to a cached controller.
To that end I used a modified version of the library, one with multiple injections
for reporting delays of execution, and these are the results...
The length of routing and calling was consistently under 1*10-7 second,
i.e. less than 100 nanoseconds. The only random slow-downs that I saw I would attribute
to either internal garbage collection or some resource allocation on the PC, during
which the time could momentarily jump to a whole 1 millisecond, but those aren't
really important.
The entire library came to be a mere 12KB DLL, with references only to the core
DLL-s of:
- Microsoft.CSharp
- System
- System.Core
- System.Web
This is the smallest footprint we could achieve for an assembly, and considering
the library performance, the result looks quite worthwhile.
Points of Interest
There were a few interesting things I found during implementation of this demo,
they are listed in detail in chapter Implementation.
History
- May 08, 2012. First version of the library, and initial draft of the article.
- May 08, 2012. Second revision of the library: Added support for prefix
segments in
BaseController
, plus improved its documentation.
- May 09, 2012. Improved article formatting, added Contents table, plus a
few more details.
- May 09, 2012. Added more details about supporting arrays of mixed types.
- May 10, 2012. Minor improvements in code and documentation.
- May 11, 2010. Added support for multiple assemblies. In consequence, had
to place the library into its own assembly to simplify its reuse and make cross-assembly
use possible.
- May 15, 2012. Added major changes to the demo source and documentation as listed below.
- Made property
SimpleHandler.arraySeparator
public, and added many helpful details in its declaration.
- Added method
SimpleHandler.CleanSegment
to prepare any segment for further processing. As a result, it is now
allowed to pass a request with spaces in it, which will be stripped automatically. But most importantly, use of requests
with combination of letter cases won't cause a separate controller instance per letter-case anymore.
- Added a comprehensive demo - a simple web application that can be run from Visual Studio and show how everything works.
The demo also includes a simple benchmark for measuring speed of Ajax requests going through
SimpleRouter
.
-
May 20, 2012. Made huge changes to the library. In fact, once I finished changing it I started updating the article
only to realize it now needs complete rewrite, for which I hope to find time in the near future. In the meantime,
I list most of the changes I made here below, and suggest anyone who would like to use to rely more on the source
that's enclosed, as it is quite simple and most up-to-date. And if you have any questions, I'd love answering them
in the comments section. Thank you!
- Added support for the same controller name but in different namespaces or even different assemblies.
- Added support for use of a custom namespace, so one can be overridden based on prefix segments or anything
else you may like. See method
OnValidatePrefix
.
- Added validation of prefix segments (method
OnValidatePrefix
), so one can choose a custom action based
on the prefix contents, and even override the prefix itself, if needs to be.
- Added support for explicit action call, when name of controller and action are specified. This becomes
necessary in cases, like forwarding call to a known controller/action, like in case of an error.
- Added support for timeouts, to expire long-unused controllers (method
SetTimeout
).
- A number of changes were made to the demo controllers.
- Changed the caching logic for controllers, so that only one controller is created for any action
it implements.
- Added thread safety throughout the library.
-
June 10, 2012. Added chapter URL Filtering that explains:
- How and why we used the filters for the demo application, and how to change them to handle any request.
- Why class
HttpFileCache
was included into project ServerTest, plus how and when to use it.