Dynamic Image Generation, ASP.NET Controllers, Routing, IHttpHandlers, and runAllManagedModulesForAllRequests
Referred URL
Often folks want to dynamically generate stuff with ASP.NET. The want to dynamically generate PDFs, GIFs, PNGs, CSVs, and lots more. It's easy to do this, but there's a few things to be aware of if you want to keep things as simple and scalable as possible.
You need to think about the whole pipeline as any HTTP request comes in. The goal is to have just the minimum number of things run to do the job effectively and securely, but you also need to think about "who sees the URL and when."
This diagram isn't meant to be exhaustive, but rather give a general sense of when things happen.
Modules can see any request if they are plugged into the pipeline. There are native modules written in C++ and managed modules written in .NET. Managed modules are run anytime a URL ends up being processed by ASP.NET or if "RAMMFAR" is turned on.
<
system.webServer
>
<
modules
runAllManagedModulesForAllRequests
=
"true"
/>
</
system.webServer
>
You want to avoid having this option turned on if your configuration and architecture can handle it. This does exactly what it says. All managed modules will run for all requests. That means *.* folks. PNGs, PDFs, everything including static files ends up getting seen by ASP.NET and the full pipeline. If you can let IIS handle a request before ASP.NET sees it, that's better.
Remember that the key to scaling is to do as little as possible. You can certainly make a foo.aspx in ASP.NET Web Forms page and have it dynamically generate a graphic, but there's some non-zero amount of overhead involved in the creation of the page and its lifecycle. You can make a MyImageController in ASP.NET MVC but there's some overhead in the Routing that chopped up the URL and decided to route it to the Controller. You can create just an HttpHandler or ashx. The result in all these cases is that an image gets generated but if you can get in and get out as fast as you can it'll be better for everyone. You can route the HttpHandler with ASP.NET Routing or plug it into web.config directly.
Works But...Dynamic Images with RAMMFAR and ASP.NET MVC
A customer wrote me who was using ASP.NET Routing (which is an HttpModule) and a custom routing handler to generate images like this:
routes.Add(
new
Route(
"images/mvcproducts/{ProductName}/default.png"
,
new
CustomPNGRouteHandler()));
Then they have a IRouteHandler that just delegates to an HttpHandler anyway:
public
class
CustomPNGRouteHandler : IRouteHandler
{
public
System.Web.IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return
new
CustomPNGHandler(requestContext);
}
}
Note the {ProductName} route data in the route there. The customer wants to be able to put anything in that bit. if I visithttp://localhost:9999/images/mvcproducts/myproductname/default.png I see this image...
Generated from this simple HttpHandler:
public
class
CustomPNGHandler : IHttpHandler
{
public
bool
IsReusable {
get
{
return
false
; } }
protected
RequestContext RequestContext {
get
;
set
; }
public
CustomPNGHandler():
base
(){}
public
CustomPNGHandler(RequestContext requestContext)
{
this
.RequestContext = requestContext;
}
public
void
ProcessRequest(HttpContext context)
{
using
(var rectangleFont =
new
Font(
"Arial"
, 14, FontStyle.Bold))
using
(var bitmap =
new
Bitmap(320, 110, PixelFormat.Format24bppRgb))
using
(var g = Graphics.FromImage(bitmap))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
var backgroundColor = Color.Bisque;
g.Clear(backgroundColor);
g.DrawString(
"This PNG was totally generated"
, rectangleFont, SystemBrushes.WindowText,
new
PointF(10, 40));
context.Response.ContentType =
"image/png"
;
bitmap.Save(context.Response.OutputStream, ImageFormat.Png);
}
}
}
The benefits of using MVC is that handler is integrated into your routing table. The bad thing is that doing this simple thing requires RAMMFAR to be on. Every module sees every request now so you can generate your graphic. Did you want that side effect? The bold is to make you pay attention, not scare you. But you do need to know what changes you're making that might affect the whole application pipeline.
(As an aside, if you're a big site doing dynamic images, you really should have your images on their own cookieless subdomain in the cloud somewhere with lots of caching, but that's another article).
So routing to an HttpHandler (or an MVC Controller) is an OK solution but it's worth exploring to see if there's an easier way that would involve fewer moving parts. In this case the they really want the file to have the extension *.png rather than *.aspx (page) or *.ashx (handler) as it they believe it affects their image's SEO in Google Image search.
Better: Custom HttpHandlers
Remember that HttpHandlers are targeted to a specific path, file or wildcard and HttpModules are always watching. Why not use an HttpHandler directly and plug it in at the web.config level and set runAllManagedModulesForAllRequests="false"?
<
system.webServer
>
<
handlers
>
<
add
name
=
"pngs"
verb
=
"*"
path
=
"images/handlerproducts/*/default.png"
type
=
"DynamicPNGs.CustomPNGHandler, DynamicPNGs"
preCondition
=
"managedHandler"
/>
</
handlers
>
<
modules
runAllManagedModulesForAllRequests
=
"false"
/>
</
system.webServer
>
Note how I have a * there in part of the URL? Let's try hitting http://localhost:37865/images/handlerproducts/myproductname/default.png. It still works.
This lets us not only completely bypass the managed ASP.NET Routing system but also remove RAMMFAR so fewer modules are involved for other requests. By default, managed modules will only run for requests that ended up mapped to the managed pipeline and that's almost always requests with an extension. You may need to be aware of routing if you have a "greedy route" that might try to get ahold of your URL. You might want an IgnoreRoute. You also need to be aware of modules earlier in the process that have a greedy BeginRequest.
The customer could setup ASP.NET and IIS to route request for *.png to ASP.NET, but why not be as specific as possible so that the minimum number of requests is routed through the managed pipeline? Don't do more work than you need to.
What about extensionless URLs?
Getting extensionless URLs working on IIS6 was tricky before and lots of been written on it. Early on in IIS6 and ASP.NET MVC you'd map everything *.* to managed code. ASP.NET Routing used to require RAMFARR set to true until the Extensionless URL feature was created.
Extentionless URLs support was added in this KB http://support.microsoft.com/kb/980368 and ships with ASP.NET MVC 4. If you have ASP.NET MVC 4, you have Extentionless URLs on your development machine. But your server may not. You may need to install this hotfix, or turn on RAMMFAR. I would rather you install the update than turn on RAMMFAR if you can avoid it. The Run All Modules options is really a wildcard mapping.
Extensionless URLs exists so you can have URLs like /home/about and not /home/about.aspx. It exists to get URLs without extensions to be seen be the managed pipelines while URLs with extensions are not seen any differently. The performance benefits of Extensionless URLs over RAMMFAR are significant.
If you have static files like CSS, JS and PNG files you really want those to be handled by IIS (and HTTP.SYS) for speed. Don't let your static files get mapped to ASP.NET if you can avoid it.
Conclusion
When you're considering any solution within the ASP.NET stack (or "One ASP.NET" as I like to call it)...
...remember that it's things like IHttpHandler that sit at the bottom and serve one request (everything comes from IHttpHandler) while it's IHttpModule that's always watching and can see every request.
In other words, and HttpHandler sees the ExecuteRequestHandler event which is just one event in the pipeline, while HttpModules can see every event they subscribe to.
0 comments