URL Mapper Handler Factory






4.17/5 (8 votes)
An IHttpHandler/IHttpHandlerFactory to map requests.
Introduction
In most enterprise environments, there are usually lots of web applications from various origins (bought as a ready-made product or built for a specific purpose) that are published in most different mediums (URLs sent by e-mail, enterprise portals, rich client applications hosting web browsers, etc.).
In such an environment, coupling every caller with a specific entry point that can change with time and versions leads to a maintenance nightmare.
Solution
The solution to this problem is using a smart URL mapper that is used as the entry point and maps the first request to the real entry point of the application.
In order to make this mapper versatile, it should have a way to transform the request to the mapper into a request to the application. Such transformations should be configurable and allow some logic in its working, and the best technology to accomplish that is using XSLT.
Implementation
The mapper is implemented as an HTTP Handler that is created by an HTTP Handler Factory.
The URL Mapper Handler Factory
This is the class responsible to provide the ASP.NET engine with the HTTP Handler that will handle the request.
To enhance performance, the handler factory keeps a stack of idle handles for each URL.
private readonly Dictionary<string, Stack<UrlMapperHandler>>
globalCache = new Dictionary<string, Stack<UrlMapperHandler>>();
When a handler is requested, the stack for the corresponding URL is checked for an available instance. If one exists, it's returned; otherwise, a new one is created and returned.
public System.Web.IHttpHandler GetHandler(System.Web.HttpContext context,
string requestType, string url, string pathTranslated)
{
lock (globalCache)
{
Stack<UrlMapperHandler> factoryCache;
if (globalCache.TryGetValue(pathTranslated, out factoryCache))
{
if (factoryCache.Count != 0)
{
return factoryCache.Pop();
}
}
}
return new UrlMapperHandler(pathTranslated);
}
When the handler is done with the request, if it's a reusable handler, it's pushed into the idle stack.
public void ReleaseHandler(System.Web.IHttpHandler handler)
{
if (handler.IsReusable)
{
UrlMapperHandler urlMapperHandler = (UrlMapperHandler)handler;
lock (globalCache)
{
Stack<UrlMapperHandler> factoryCache;
if (!globalCache.TryGetValue(pathTranslated, out factoryCache))
{
globalCache.Add(factoryCache = new Stack<UrlMapperHandler>());
}
factoryCache.Push(urlMapperHandler);
}
}
}
The URL Mapper Handler
This is the class that handles the request.
When an instance is created, an instance of a XslCompiledTransform
is created and the transformation document is loaded into it.
public UrlMapperHandler(string pathTranslated)
{
this.pathTranslated = pathTranslated;
this.xslt = new XslCompiledTransform();
this.xslt.Load(this.pathTranslated);
}
When the request processing starts, an XML Document is created and loaded with the request information. The query string items are loaded into the QueryString
element and the form items are loaded into the Form
element according to the following schema.
public void ProcessRequest(HttpContext context)
{
// Create the request document
XmlDocument requestDocument = new XmlDocument();
requestDocument.AppendChild(requestDocument.CreateElement("Request"));
FillItems(requestDocument, "QueryString", context.Request.QueryString);
FillItems(requestDocument, "Form", context.Request.Form);
...
}
The request is then transformed using the previously created XslCompiledTransform
instance, providing a XsltArgumentList
that will add request specific parameters to the transformation. The transformed document will have the following schema:
public void ProcessRequest(HttpContext context)
{
...
// Load the transformation parameters
XsltArgumentList arguments = new XsltArgumentList();
arguments.AddParam("UserName", string.Empty,
(context.User == null) ? string.Empty : context.User.Identity.Name);
arguments.AddParam("ApplicationPath",
string.Empty, context.Request.ApplicationPath);
arguments.AddParam("AppRelativeCurrentExecutionFilePath",
string.Empty,
context.Request.AppRelativeCurrentExecutionFilePath);
// Transform the request
byte[] buffer;
using (MemoryStream mappedRequestStream = new MemoryStream())
{
using (XmlTextWriter mappedRequestWriter =
new XmlTextWriter(mappedRequestStream, Encoding.UTF8))
{
this.xslt.Transform(requestDocument, arguments,
mappedRequestWriter);
}
buffer = mappedRequestStream.GetBuffer();
}
// Load the mapped request document
XPathDocument mappedRequest;
using (MemoryStream mappedRequestStream = new MemoryStream(buffer))
{
mappedRequest = new XPathDocument(mappedRequestStream);
}
...
}
Finally, the mapped request is created and executed based on the mapped request document.
public void ProcessRequest(HttpContext context)
{
...
// Build and execute the mapped request
XPathNavigator navigator =
mappedRequest.CreateNavigator().SelectSingleNode("/MappedRequest");
string path = navigator.GetAttribute("path", string.Empty);
if (!string.IsNullOrEmpty(path))
{
StringBuilder sbPath = new StringBuilder(path);
// Build the query string
bool pathHasQuery = path.IndexOf('?') >= 0;
XPathNodeIterator queryStringNodes =
navigator.Select("/MappedRequest/QueryString/Item");
while (queryStringNodes.MoveNext())
{
string item =
queryStringNodes.Current.GetAttribute("name", string.Empty);
if (!string.IsNullOrEmpty(item))
{
if (pathHasQuery)
{
sbPath.Append('&');
}
else
{
sbPath.Append('?');
pathHasQuery = true;
}
sbPath.Append(HttpUtility.UrlEncode(item));
sbPath.Append('=');
sbPath.Append(HttpUtility.UrlEncode(queryStringNodes.Current.Value));
}
}
// Set the HttpContext items
XPathNodeIterator httpContextNodes =
navigator.Select("/MappedRequest/HttpContext/Item");
while (httpContextNodes.MoveNext())
{
string item = httpContextNodes.Current.GetAttribute("name", string.Empty);
if (!string.IsNullOrEmpty(item))
{
context.Items[item] = httpContextNodes.Current.Value;
}
}
// Execute the mapped request
context.Server.Execute(sbPath.ToString(), false);
}
}
Usage and Configuration
In order to use this URL mapper, the HTTP Handler Factory must be registered in the web.config of the site in the httpHandlers
section and in the IIS mappings.
<configuration>
<system.web>
<httpHandlers>
<add verb="GET" path="*.srvx"
type="Pajocomo.UrlMapper.UrlMapperHandlerFactory, Pajocomo.UrlMapper"/>
</httpHandlers>
</system.web>
</configuration>
Improvements
To maintain performance in the creation of handler instances and to reduce memory usage, a better cache mechanism should be used to expire instances based on idle time and changes in the mapper definition file.