Can I use HttpHandler to fake the existence of aspx pages? - asp.net

I am building a web site with ASP.NET 3.5, and most of the site structure is static enough to create a folder structure and aspx pages. However, the site administrators want the ability to add new pages to different sections of the site through a web interface and using a WYSIWYG editor. I am using nested master pages to give the different sections of the site their own menus. What I would like to do is have a generic page under each section of the site that uses the appropriate master page and has a place holder for content that could be loaded from a database. I would also like these "fake" pages to have a url like any other aspx page, as if they had corresponding files on the server. So rather than have my url be:
http://mysite.com/subsection/gerenicconent.aspx?contentid=1234
it would be something like:
http://mysite.com/subsection/somethingmeaningful.aspx
The problem is that somethingmeaningful.aspx does not exist, because the administrator created it through the web UI, and the content is stored in the database. What I'm thinking is that I'll implement an HTTP handler that handles requests for aspx files. In that handler, I'll check to see if the URL that was requested is an actual file or one of my "fake pages". If it is a request for a fake page, I'll re-route the request to the generic content page for the appropriate section, change the query string to request the appropriate data from the database, and rewrite the URL so that it looks to the user as if the fake page really exists. The problem I'm having right now is that I can't figure out how to route the request to the default handler for aspx pages. I tried to instantiate a PageHandlerFactory, but the constuctor is protected internal. Is there any way for me to tell my HttpHandler to call the HttpHandler that would normal be used to process a request? My handler code currently looks like this:
using System.Web;
using System.Web.UI;
namespace HandlerTest
{
public class FakePageHandler : IHttpHandler
{
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
if(RequestIsForFakedPage(context))
{
// reroute the request to the generic page and rewrite the URL
PageHandlerFactory factory = new PageHandlerFactory(); // this won't compile because the constructor is protected internal
factory.GetHandler(context, context.Request.RequestType, GetGenericContentPath(context), GetPhysicalApplicationPath(context)).ProcessRequest(context);
}
else
{
// route the request to the default handler for aspx pages
PageHandlerFactory factory = new PageHandlerFactory();
factory.GetHandler(context, context.Request.RequestType, context.Request.Path, context.Request.PhysicalPath).ProcessRequest(context);
}
}
public string RequestForPageIsFaked(HttpContext context)
{
// TODO
}
public string GetGenericContentPath(HttpContext context)
{
// TODO
}
public string GetPhysicalApplicationPath(HttpContext context)
{
// TODO
}
}
}
I still have some work to do to determine if the request is for a real page, and I haven't rewritten any URLs yet, but is something like this possible? Is there another way to create a PageHandlerFactory other than calling its constructor? Is there any way I can route the request up to the "normal" HttpHandler for an aspx page? I'd basically be saying "process this ASPX request as you normally would."

If you are using 3.5, look into using asp.net routing.
http://msdn.microsoft.com/en-us/library/cc668201.aspx

You would be better off using an http module for this, as in this case you can use the RewritePath method to route the request for fake pages, and do nothing for actual pages which will allow them to be processed as normal.
There is a good explanation of this here which also covers the benefits of using IIS 7.0 if that is an option for you.

I've just pulled this off a similar system we've just written.
This method takes care of physical pages and "fake" pages. You'll be able to ascertan how this fits with your fake page schema, I'm sure.
public class AspxHttpHandler : IHttpHandlerFactory
{
#region ~ from IHttpHandlerFactory ~
public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated)
{
string url=context.Request.Url.AbsolutePath;
string[] portions = url.Split(new char[] { '/', '\\' });
// gives you the path, i presume this will help you identify the section and page
string serverSidePage=Path.Combine(context.Server.MapPath("~"),url);
if (File.Exists(serverSidePage))
{
// page is real
string virtualPath = context.Request.Url.AbsolutePath;
string inputFile = context.Server.MapPath(virtualPath);
try
{
// if it's real, send in the details to the ASPX compiler
return PageParser.GetCompiledPageInstance(virtualPath, inputFile, context);
}
catch (Exception ex)
{
throw new ApplicationException("Failed to render physical page", ex);
}
}
else
{
// page is fake
// need to identify a page that exists which you can use to compile against
// here, it is CMSTaregtPage - it can use a Master
string inputFile = context.Server.MapPath("~/CMSTargetPage.aspx");
string virtualPath = "~/CMSTargetPage.aspx";
// you can also add things that the page can access vai the Context.Items collection
context.Items.Add("DataItem","123");
return PageParser.GetCompiledPageInstance(virtualPath, inputFile, context);
}
public void ReleaseHandler(IHttpHandler handler)
{
}

Related

Can't handle routing correctly in mixed VS environment

I am creating a new project in Visual Studio using the SPA template. My goal is to have an Angular/SPA application that will "host"/contain some legacy applications that will eventually be modernized/migrated. So, I have an iframe on a page in my SPA app, and when a menu item is clicked, I want to load one of the legacy ASP.NET apps in that iframe (it has to be in an iframe, as the legacy site used them, and its architecture relies on them).
I am having trouble getting the routing right. The SPA template defines a DefaultRoute class like this (I changed RouteExistingFiles to true):
public class DefaultRoute : Route
{
public DefaultRoute()
: base("{*path}", new DefaultRouteHandler()) {
this.RouteExistingFiles = true;
}
}
and I have edited the RouteConfig.cs file to ignore "aspx" page requests, like this:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes) {
routes.Ignore("*.aspx");
routes.Add("Default", new DefaultRoute());
}
}
The default route handler that is defined, looks like this:
public class DefaultRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext) {
// Use cases:
// ~/ -> ~/views/index.cshtml
// ~/about -> ~/views/about.cshtml or ~/views/about/index.cshtml
// ~/views/about -> ~/views/about.cshtml
// ~/xxx -> ~/views/404.cshtml
var filePath = requestContext.HttpContext.Request.AppRelativeCurrentExecutionFilePath;
if (filePath == "~/") {
filePath = "~/views/index.cshtml";
}
else {
if (!filePath.StartsWith("~/views/", StringComparison.OrdinalIgnoreCase)) {
filePath = filePath.Insert(2, "views/");
}
if (!filePath.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase)) {
filePath = filePath += ".cshtml";
}
}
var handler = WebPageHttpHandler.CreateFromVirtualPath(filePath); // returns NULL if .cshtml file wasn't found
if (handler == null) {
requestContext.RouteData.DataTokens.Add("templateUrl", "/views/404");
handler = WebPageHttpHandler.CreateFromVirtualPath("~/views/404.cshtml");
}
else {
requestContext.RouteData.DataTokens.Add("templateUrl", filePath.Substring(1, filePath.Length - 8));
}
return handler;
}
}
The directory structure is like this:
MySPA
SPA <- contains the SPA application (.net 4.5)
Legacy <- contains the legacy applications (.net 3.0)
In IIS, I have the legacy folder set as a virtual directory (subdirectory) within the SPA application.
How do I set up routing so that, when a menu item is clicked, and the request is sent (containing a url that has query string information) for an .aspx page, the request can be routed to the legacy application?
I have solved this issue. The issue was caused by several problems. But, the problem that most pertains to my information above was this...I needed to find the correct way to ignore the requests for the legacy aspx pages from within the routing code of the new site. So, in the RouteConfig.cs file, I placed this ignore statement as the first line of the RegisterRoute function:
routes.Ignore("{*allaspx}", new { allaspx = #".*\.aspx(/.*)?"});
That corrected the main issue, there were other minor issues that confused the situation but, I have identified what they are and I am working on those. So, ignoring the legacy urls in the routing functionality was the correct solution.

Call the default asp.net HttpHandler from a custom handler

I'm adding ASP.NET routing to an older webforms app. I'm using a custom HttpHandler to process everything. In some situations I would like to map a particular path back to an aspx file, so I need to just pass control back to the default HttpHandler for asp.net.
The closest I've gotten is this
public void ProcessRequest(HttpContext context) {
// .. when we decide to pass it on
var handler = new System.Web.UI.Page();
handler.ProcessRequest(context);
MemoryStream steam = new MemoryStream();
StreamWriter writer = new StreamWriter(stream);
HtmlTextWriter htmlWriter = new HtmlTextWriter(writer);
handler.RenderControl(htmlWriter);
// write headers, etc. & send stream to Response
}
It doesn't do anything, there's nothing output to the stream. MS's documentation for System.Web.UI.Page (as an IHttpHandler) say something to the effect of "do not call the ProcessRequest method. It's for internal use."
From looking around it seems like you can do this with MVC, e.g. : MvcHttpHandler doesn't seem to implement IHttpHandler
There is also this thing System.Web.UI.PageHandlerFactory which appears that it would just produce a Page handler for an aspx file, but it's internal and I can't use it directly.
This page: http://msdn.microsoft.com/en-us/library/bb398986.aspx refers to the "default asp.net handler" but does not identify a class or give any indication how one might use it.
Any ideas on how I can do this? Is it possible?
Persistence pays off! This actually works, and since this information seems to be available pretty much nowhere I thought I'd answer my own question. Thanks to Robert for this post on instantiating things with internal constructors, this is the key.
http://www.rvenables.com/2009/08/instantiating-classes-with-internal-constructors/
public void ProcessRequest(HttpContext context) {
// the internal constructor doesn't do anything but prevent you from instantiating
// the factory, so we can skip it.
PageHandlerFactory factory =
(PageHandlerFactory)System.Runtime.Serialization.FormatterServices
.GetUninitializedObject(typeof(System.Web.UI.PageHandlerFactory));
string newTarget = "default.aspx";
string newQueryString = // whatever you want
string oldQueryString = context.Request.QueryString.ToString();
string queryString = newQueryString + oldQueryString!="" ?
"&" + newQueryString :
"";
// the 3rd parameter must be just the file name.
// the 4th parameter should be the physical path to the file, though it also
// works fine if you pass an empty string - perhaps that's only to override
// the usual presentation based on the path?
var handler = factory.GetHandler(context, "GET",newTarget,
context.Request.MapPath(context,newTarget));
// Update the context object as it should appear to your page/app, and
// assign your new handler.
context.RewritePath(newTarget , "", queryString);
context.Handler = handler;
// .. and done
handler.ProcessRequest(context);
}
... and like some small miracle, an aspx page processes & renders completely in-process without the need to redirect.
I expect this will only work in IIS7.
I'm you're using Routing in webforms you should be able to just add an ignore route for the specific .aspx files you want. This will then be handled by the default HttpHandler.
http://msdn.microsoft.com/en-us/library/dd505203.aspx
Another option is to invert the logic by handling the cases in which you do NOT want to return the default response and remap the others to your own IHttpHandler. Whenever myCondition is false, the response will be the "default". The switch is implemented as an IHttpModule:
public class SwitchModule: IHttpModule
{
public void Init(HttpApplication context)
{
context.PostAuthenticateRequest += app_PostAuthenticateRequest;
}
void app_PostAuthenticateRequest(object sender, EventArgs e)
{
// Check for whatever condition you like
if (true)
HttpContext.Current.RemapHandler(new CustomHandler());
}
public void Dispose()
}
internal class CustomHandler: IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
context.Response.Write("hallo");
}
public bool IsReusable { get; }
}

ASP.NET: How to get the virtual path of a file from a generic handler?

How can i resolve a virtual path to a file into a path, suitable for the browser, from within a generic .ashx handler?
e.g. i want to convert:
~/asp/ClockState.aspx
into
/NextAllowed/asp/ClockState.aspx
If i were a WebForm Page, i could call ResolveUrl:
Page.ResolveUrl("~/asp/ClockState.aspx")
which resolves to:
/NextAllowed/asp/ClockState.aspx
But i'm not a WebForm Page, i'm a generic handler. You know, that IHttpHandler object with all kinds of things injected:
public class ResetClock : IHttpHandler
{
public void ProcessRequest (HttpContext context)
{
//[process stuff]
//Redirect client
context.Response.Redirect("~/asp/ClockState.aspx", true);
}
public bool IsReusable { get { return false; } }
}
You can use the VirtualPathUtility class to do this. This contains various methods for working with paths. The one you need is ToAbsolute(), which will convert a relative path to an absolute one.
var path = VirtualPathUtility.ToAbsolute("~/asp/ClockState.aspx");
However, you can use the tilde in Response.Redirect calls anyway, so the following would still work:
Response.Redirect("~/asp/ClockState.aspx");
You do not need to convert the URL to an absolute path before using Response.Redirect.

Can ASP.NET Routing be used to create "clean" URLs for .ashx (IHttpHander) handlers?

I have some REST services using plain old IHttpHandlers. I'd like to generate cleaner URLs, so that I don't have the .ashx in the path. Is there a way to use ASP.NET routing to create routes that map to ashx handlers? I've seen these types of routes previously:
// Route to an aspx page
RouteTable.Routes.MapPageRoute("route-name",
"some/path/{arg}",
"~/Pages/SomePage.aspx");
// Route for a WCF service
RouteTable.Routes.Add(new ServiceRoute("Services/SomeService",
new WebServiceHostFactory(),
typeof(SomeService)));
Trying to use RouteTable.Routes.MapPageRoute() generates an error (that the handler does not derive from Page). System.Web.Routing.RouteBase only seems to have 2 derived classes: ServiceRoute for services, and DynamicDataRoute for MVC. I'm not sure what MapPageRoute() does (Reflector doesn't show the method body, it just shows "Performance critical to inline this type of method across NGen image boundaries").
I see that RouteBase is not sealed, and has a relatively simple interface:
public abstract RouteData GetRouteData(HttpContextBase httpContext);
public abstract VirtualPathData GetVirtualPath(RequestContext requestContext,
RouteValueDictionary values);
So perhaps I can make my own HttpHandlerRoute. I'll give that a shot, but if anyone knows of an existing or built-in way of mapping routes to IHttpHandlers, that would be great.
Ok, I've been figuring this out since I originally asked the question, and I finally have a solution that does just what I want. A bit of up front explanation is due, however. IHttpHandler is a very basic interface:
bool IsReusable { get; }
void ProcessRequest(HttpContext context)
There is no built in property for accessing the route data, and the route data also can't be found in the context or the request. A System.Web.UI.Page object has a RouteData property , ServiceRoutes do all the work of interpreting your UriTemplates and passing the values to the correct method internally, and ASP.NET MVC provides its own way of accessing the route data. Even if you had a RouteBase that (a) determined if the incoming url was a match for your route and (b) parsed the url to extract all of the individual values to be used from within your IHttpHandler, there's no easy way to pass that route data to your IHttpHandler. If you want to keep your IHttpHandler "pure", so to speak, it takes responsibility for dealing with the url, and how to extract any values from it. The RouteBase implementation in this case is only used to determine if your IHttpHandler should be used at all.
One problem remains, however. Once the RouteBase determines that the incoming url is a match for your route, it passes off to an IRouteHandler, which creates the instances of the IHttpHandler you want to handle your request. But, once you're in your IHttpHandler, the value of context.Request.CurrentExecutionFilePath is misleading. It's the url that came from the client, minus the query string. So it's not the path to your .ashx file. And, any parts of your route that are constant (such as the name of the method) will be part of that execution file path value. This can be a problem if you use UriTemplates within your IHttpHandler to determine which specific method within your IHttpHandler should handing the request.
Example: If you had a .ashx handler at /myApp/services/myHelloWorldHandler.ashx
And you had this route that mapped to the handler: "services/hello/{name}"
And you navigated to this url, trying to call the SayHello(string name) method of your handler:
http://localhost/myApp/services/hello/SayHello/Sam
Then your CurrentExecutionFilePath would be: /myApp/services/hello/Sam. It includes parts of the route url, which is a problem. You want the execution file path to match your route url. The below implementations of RouteBase and IRouteHandler deal with this problem.
Before I paste the 2 classes, here's a very simple usage example. Note that these implementations of RouteBase and IRouteHandler will actually work for IHttpHandlers that don't even have a .ashx file, which is pretty convenient.
// A "headless" IHttpHandler route (no .ashx file required)
RouteTable.Routes.Add(new GenericHandlerRoute<HeadlessService>("services/headless"));
That will cause all incoming urls that match the "services/headless" route to be handed off to a new instance of the HeadlessService IHttpHandler (HeadlessService is just an example in this case. It would be whatever IHttpHandler implementation you wanted to pass off to).
Ok, so here are the routing class implementations, comments and all:
/// <summary>
/// For info on subclassing RouteBase, check Pro Asp.NET MVC Framework, page 252.
/// Google books link: http://books.google.com/books?id=tD3FfFcnJxYC&pg=PA251&lpg=PA251&dq=.net+RouteBase&source=bl&ots=IQhFwmGOVw&sig=0TgcFFgWyFRVpXgfGY1dIUc0VX4&hl=en&ei=z61UTMKwF4aWsgPHs7XbAg&sa=X&oi=book_result&ct=result&resnum=6&ved=0CC4Q6AEwBQ#v=onepage&q=.net%20RouteBase&f=false
///
/// It explains how the asp.net runtime will call GetRouteData() for every route in the route table.
/// GetRouteData() is used for inbound url matching, and should return null for a negative match (the current requests url doesn't match the route).
/// If it does match, it returns a RouteData object describing the handler that should be used for that request, along with any data values (stored in RouteData.Values) that
/// that handler might be interested in.
///
/// The book also explains that GetVirtualPath() (used for outbound url generation) is called for each route in the route table, but that is not my experience,
/// as mine used to simply throw a NotImplementedException, and that never caused a problem for me. In my case, I don't need to do outbound url generation,
/// so I don't have to worry about it in any case.
/// </summary>
/// <typeparam name="T"></typeparam>
public class GenericHandlerRoute<T> : RouteBase where T : IHttpHandler, new()
{
public string RouteUrl { get; set; }
public GenericHandlerRoute(string routeUrl)
{
RouteUrl = routeUrl;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
// See if the current request matches this route's url
string baseUrl = httpContext.Request.CurrentExecutionFilePath;
int ix = baseUrl.IndexOf(RouteUrl);
if (ix == -1)
// Doesn't match this route. Returning null indicates to the asp.net runtime that this route doesn't apply for the current request.
return null;
baseUrl = baseUrl.Substring(0, ix + RouteUrl.Length);
// This is kind of a hack. There's no way to access the route data (or even the route url) from an IHttpHandler (which has a very basic interface).
// We need to store the "base" url somewhere, including parts of the route url that are constant, like maybe the name of a method, etc.
// For instance, if the route url "myService/myMethod/{myArg}", and the request url were "http://localhost/myApp/myService/myMethod/argValue",
// the "current execution path" would include the "myServer/myMethod" as part of the url, which is incorrect (and it will prevent your UriTemplates from matching).
// Since at this point in the exectuion, we know the route url, we can calculate the true base url (excluding all parts of the route url).
// This means that any IHttpHandlers that use this routing mechanism will have to look for the "__baseUrl" item in the HttpContext.Current.Items bag.
// TODO: Another way to solve this would be to create a subclass of IHttpHandler that has a BaseUrl property that can be set, and only let this route handler
// work with instances of the subclass. Perhaps I can just have RestHttpHandler have that property. My reticence is that it would be nice to have a generic
// route handler that works for any "plain ol" IHttpHandler (even though in this case, you have to use the "global" base url that's stored in HttpContext.Current.Items...)
// Oh well. At least this works for now.
httpContext.Items["__baseUrl"] = baseUrl;
GenericHandlerRouteHandler<T> routeHandler = new GenericHandlerRouteHandler<T>();
RouteData rdata = new RouteData(this, routeHandler);
return rdata;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
// This route entry doesn't generate outbound Urls.
return null;
}
}
public class GenericHandlerRouteHandler<T> : IRouteHandler where T : IHttpHandler, new()
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new T();
}
}
I know this answer has been quite long winded, but it was not an easy problem to solve. The core logic was easy enough, the trick was to somehow make your IHttpHandler aware of the "base url", so that it could properly determine what parts of the url belong to the route, and what parts are actual arguments for the service call.
These classes will be used in my upcoming C# REST library, RestCake. I hope that my path down the routing rabbit hole will help anyone else who decides to RouteBase, and do cool stuff with IHttpHandlers.
I actually like Joel's solution better, as it doesn't require you to know the type of handler while you're trying to setup your routes. I'd upvote it, but alas, I haven't the reputation required.
I actually found a solution which I feel is better than both mentioned. The original source code I derived my example from can be found linked here http://weblogs.asp.net/leftslipper/archive/2009/10/07/introducing-smartyroute-a-smarty-ier-way-to-do-routing-in-asp-net-applications.aspx.
This is less code, type agnostic, and fast.
public class HttpHandlerRoute : IRouteHandler {
private String _VirtualPath = null;
public HttpHandlerRoute(String virtualPath) {
_VirtualPath = virtualPath;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext) {
IHttpHandler httpHandler = (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath(_VirtualPath, typeof(IHttpHandler));
return httpHandler;
}
}
And a rough example of use
String handlerPath = "~/UploadHandler.ashx";
RouteTable.Routes.Add(new Route("files/upload", new HttpHandlerRoute(handlerPath)));
EDIT: I just edited this code because I had some issues with the old one. If you're using the old version please update.
This thread is a bit old but I just re-wrote some of the code here to do the same thing but on a more elegant way, using an extension method.
I'm using this on ASP.net Webforms, and I like to have the ashx files on a folder and being able to call them either using routing or a normal request.
So I pretty much grabbed shellscape's code and made an extension method that does the trick. At the end I felt that I should also support passing the IHttpHandler object instead of its Url, so I wrote and overload of the MapHttpHandlerRoute method for that.
namespace System.Web.Routing
{
public class HttpHandlerRoute<T> : IRouteHandler where T: IHttpHandler
{
private String _virtualPath = null;
public HttpHandlerRoute(String virtualPath)
{
_virtualPath = virtualPath;
}
public HttpHandlerRoute() { }
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return Activator.CreateInstance<T>();
}
}
public class HttpHandlerRoute : IRouteHandler
{
private String _virtualPath = null;
public HttpHandlerRoute(String virtualPath)
{
_virtualPath = virtualPath;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
if (!string.IsNullOrEmpty(_virtualPath))
{
return (IHttpHandler)System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(_virtualPath, typeof(IHttpHandler));
}
else
{
throw new InvalidOperationException("HttpHandlerRoute threw an error because the virtual path to the HttpHandler is null or empty.");
}
}
}
public static class RoutingExtension
{
public static void MapHttpHandlerRoute(this RouteCollection routes, string routeName, string routeUrl, string physicalFile, RouteValueDictionary defaults = null, RouteValueDictionary constraints = null)
{
var route = new Route(routeUrl, defaults, constraints, new HttpHandlerRoute(physicalFile));
routes.Add(routeName, route);
}
public static void MapHttpHandlerRoute<T>(this RouteCollection routes, string routeName, string routeUrl, RouteValueDictionary defaults = null, RouteValueDictionary constraints = null) where T : IHttpHandler
{
var route = new Route(routeUrl, defaults, constraints, new HttpHandlerRoute<T>());
routes.Add(routeName, route);
}
}
}
I'm putting it inside the same namespace of all the native routing objects so it will be automatically available.
So to use this you just have to call:
// using the handler url
routes.MapHttpHandlerRoute("DoSomething", "Handlers/DoSomething", "~/DoSomething.ashx");
Or
// using the type of the handler
routes.MapHttpHandlerRoute<MyHttpHanler>("DoSomething", "Handlers/DoSomething");
Enjoy,
Alex
Yeah, I noticed that, too. Perhaps there is a built-in ASP.NET way to do this, but the trick for me was to create a new class derived from IRouteHandler:
using System;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Routing;
namespace MyNamespace
{
class GenericHandlerRouteHandler : IRouteHandler
{
private string _virtualPath;
private Type _handlerType;
private static object s_lock = new object();
public GenericHandlerRouteHandler(string virtualPath)
{
_virtualPath = virtualPath;
}
#region IRouteHandler Members
public System.Web.IHttpHandler GetHttpHandler(RequestContext requestContext)
{
ResolveHandler();
IHttpHandler handler = (IHttpHandler)Activator.CreateInstance(_handlerType);
return handler;
}
#endregion
private void ResolveHandler()
{
if (_handlerType != null)
return;
lock (s_lock)
{
// determine physical path of ashx
string path = _virtualPath.Replace("~/", HttpRuntime.AppDomainAppPath);
if (!File.Exists(path))
throw new FileNotFoundException("Generic handler " + _virtualPath + " could not be found.");
// parse the class name out of the .ashx file
// unescaped reg-ex: (?<=Class=")[a-zA-Z\.]*
string className;
Regex regex = new Regex("(?<=Class=\")[a-zA-Z\\.]*");
using (var sr = new StreamReader(path))
{
string str = sr.ReadToEnd();
Match match = regex.Match(str);
if (match == null)
throw new InvalidDataException("Could not determine class name for generic handler " + _virtualPath);
className = match.Value;
}
// get the class type from the name
Assembly[] asms = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly asm in asms)
{
_handlerType = asm.GetType(className);
if (_handlerType != null)
break;
}
if (_handlerType == null)
throw new InvalidDataException("Could not find type " + className + " in any loaded assemblies.");
}
}
}
}
To create a route for an .ashx:
IRouteHandler routeHandler = new GenericHandlerRouteHandler("~/somehandler.ashx");
Route route = new Route("myroute", null, null, null, routeHandler);
RouteTable.Routes.Add(route);
The code above may need to be enhanced to work with your route arguments, but it's starting point. Comments welcome.
All of these answers are very good. I love the simplicity of Mr. Meacham's GenericHandlerRouteHandler<T> class. It is a great idea to eliminate an unnecessary reference to a virtual path if you know the specific HttpHandler class. The GenericHandlerRoute<T> class is not needed, however. The existing Route class which derives from RouteBase already handles all of the complexity of route matching, parameters, etc., so we can just use it along with GenericHandlerRouteHandler<T>.
Below is a combined version with a real-life usage example that includes route parameters.
First are the route handlers. There are two included, here -- both with the same class name, but one that is generic and uses type information to create an instance of the specific HttpHandler as in Mr. Meacham's usage, and one that uses a virtual path and BuildManager to create an instance of the appropriate HttpHandler as in shellscape's usage. The good news is that .NET allows both to live side by side just fine, so we can just use whichever we want and can switch between them as we wish.
using System.Web;
using System.Web.Compilation;
using System.Web.Routing;
public class HttpHandlerRouteHandler<T> : IRouteHandler where T : IHttpHandler, new() {
public HttpHandlerRouteHandler() { }
public IHttpHandler GetHttpHandler(RequestContext requestContext) {
return new T();
}
}
public class HttpHandlerRouteHandler : IRouteHandler {
private string _VirtualPath;
public HttpHandlerRouteHandler(string virtualPath) {
this._VirtualPath = virtualPath;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext) {
return (IHttpHandler) BuildManager.CreateInstanceFromVirtualPath(this._VirtualPath, typeof(IHttpHandler));
}
}
Let's assume that we created an HttpHandler that streams documents to users from a resource outside our virtual folder, maybe even from a database, and that we want to fool the user's browser into believing that we are directly serving a specific file rather than simply providing a download (i.e., allow the browser's plug-ins to handle the file rather than forcing the user to save the file). The HttpHandler may expect a document id with which to locate the document to provide, and may expect a file name to provide to the browser -- one that may differ from the file name used on the server.
The following shows the registration of the route used to accomplish this with a DocumentHandler HttpHandler:
routes.Add("Document", new Route("document/{documentId}/{*fileName}", new HttpHandlerRouteHandler<DocumentHandler>()));
I used {*fileName} rather than just {fileName} to allow the fileName parameter to act as an optional catch-all parameter.
To create a URL for a file served by this HttpHandler, we can add the following static method to a class where such a method would be appropriate, such as in the HttpHandler class, itself:
public static string GetFileUrl(int documentId, string fileName) {
string mimeType = null;
try { mimeType = MimeMap.GetMimeType(Path.GetExtension(fileName)); }
catch { }
RouteValueDictionary documentRouteParameters = new RouteValueDictionary { { "documentId", documentId.ToString(CultureInfo.InvariantCulture) }
, { "fileName", DocumentHandler.IsPassThruMimeType(mimeType) ? fileName : string.Empty } };
return RouteTable.Routes.GetVirtualPath(null, "Document", documentRouteParameters).VirtualPath;
}
I omitted the definitions of MimeMap and and IsPassThruMimeType to keep this example simple. But these are intended to determine whether or not specific file types should provide their file names directly in the URL, or rather in a Content-Disposition HTTP header. Some file extensions could be blocked by IIS or URL Scan, or could cause code to execute that might cause problems for users -- especially if the source of the file is another user who is malicious. You could replace this logic with some other filtering logic, or omit such logic entirely if you are not exposed to this type of risk.
Since in this particular example the file name may be omitted from the URL, then, obviously, we must retrieve the file name from somewhere. In this particular example, the file name can be retrieved by performing a look-up using document id, and including a file name in the URL is intended solely to improve the user's experience. So, the DocumentHandler HttpHandler can determine if a file name was provided in the URL, and if it was not, then it can simply add a Content-Disposition HTTP header to the response.
Staying on topic, the important part of the above code block is the usage of RouteTable.Routes.GetVirtualPath() and the routing parameters to generate a URL from the Route object that we created during the route registration process.
Here's a watered-down version of the DocumentHandler HttpHandler class (much omitted for the sake of clarity). You can see that this class uses route parameters to retrieve the document id and the file name when it can; otherwise, it will attempt to retrieve the document id from a query string parameter (i.e., assuming that routing was not used).
public void ProcessRequest(HttpContext context) {
try {
context.Response.Clear();
// Get the requested document ID from routing data, if routed. Otherwise, use the query string.
bool isRouted = false;
int? documentId = null;
string fileName = null;
RequestContext requestContext = context.Request.RequestContext;
if (requestContext != null && requestContext.RouteData != null) {
documentId = Utility.ParseInt32(requestContext.RouteData.Values["documentId"] as string);
fileName = Utility.Trim(requestContext.RouteData.Values["fileName"] as string);
isRouted = documentId.HasValue;
}
// Try the query string if no documentId obtained from route parameters.
if (!isRouted) {
documentId = Utility.ParseInt32(context.Request.QueryString["id"]);
fileName = null;
}
if (!documentId.HasValue) { // Bad request
// Response logic for bad request omitted for sake of simplicity
return;
}
DocumentDetails documentInfo = ... // Details of loading this information omitted
if (context.Response.IsClientConnected) {
string fileExtension = string.Empty;
try { fileExtension = Path.GetExtension(fileName ?? documentInfo.FileName); } // Use file name provided in URL, if provided, to get the extension.
catch { }
// Transmit the file to the client.
FileInfo file = new FileInfo(documentInfo.StoragePath);
using (FileStream fileStream = file.OpenRead()) {
// If the file size exceeds the threshold specified in the system settings, then we will send the file to the client in chunks.
bool mustChunk = fileStream.Length > Math.Max(SystemSettings.Default.MaxBufferedDownloadSize * 1024, DocumentHandler.SecondaryBufferSize);
// WARNING! Do not ever set the following property to false!
// Doing so causes each chunk sent by IIS to be of the same size,
// even if a chunk you are writing, such as the final chunk, may
// be shorter than the rest, causing extra bytes to be written to
// the stream.
context.Response.BufferOutput = true;
context.Response.ContentType = MimeMap.GetMimeType(fileExtension);
context.Response.AddHeader("Content-Length", fileStream.Length.ToString(CultureInfo.InvariantCulture));
if ( !isRouted
|| string.IsNullOrWhiteSpace(fileName)
|| string.IsNullOrWhiteSpace(fileExtension)) { // If routed and a file name was provided in the route, then the URL will appear to point directly to a file, and no file name header is needed; otherwise, add the header.
context.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", HttpUtility.UrlEncode(documentInfo.FileName)));
}
int bufferSize = DocumentHandler.SecondaryBufferSize;
byte[] buffer = new byte[bufferSize];
int bytesRead = 0;
while ((bytesRead = fileStream.Read(buffer, 0, bufferSize)) > 0 && context.Response.IsClientConnected) {
context.Response.OutputStream.Write(buffer, 0, bytesRead);
if (mustChunk) {
context.Response.Flush();
}
}
}
}
}
catch (Exception e) {
// Error handling omitted from this example.
}
}
This example uses some additional custom classes, such as a Utility class to simplify some trivial tasks. But hopefully you can weed through that. The only really important part in this class with regard to the current topic, of course, is the retrieval of the route parameters from context.Request.RequestContext.RouteData. But I've seen several posts elsewhere asking how to stream large files using an HttpHandler without chewing up server memory, so it seemed like a good idea to combine examples.

Redirecting users from edit page back to calling page

I am working on a project management web application. The user has a variety of ways to display a list of tasks. When viewing a list page, they click on task and are redirected to the task edit page.
Since they are coming from a variety of ways, I am just curious as to the best way to redirect the user back to the calling page. I have some ideas, but would like to get other developers input.
Would you store the calling url in session? as a cookie? I like the concept of using an object handle the redirection.
I would store the referring URL using the ViewState. Storing this outside the scope of the page (i.e. in the Session state or cookie) may cause problems if more than one browser window is open.
The example below validates that the page was called internally (i.e. not requested directly) and bounces back to the referring page after the user submits their response.
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (Request.UrlReferrer == null)
{
//Handle the case where the page is requested directly
throw new Exception("This page has been called without a referring page");
}
if (!IsPostBack)
{
ReturnUrl = Request.UrlReferrer.PathAndQuery;
}
}
public string ReturnUrl
{
get { return ViewState["returnUrl"].ToString(); }
set { ViewState["returnUrl"] = value; }
}
protected void btn_Click(object sender, EventArgs e)
{
//Do what you need to do to save the page
//...
//Go back to calling page
Response.Redirect(ReturnUrl, true);
}
}
This message my be tagged asp.net but I think it is a platform independent issue that pains all new web developers as they seek a 'clean' way to do this.
I think the two options in achieving this are:
A param in the url
A url stored in the session
I don't like the url method, it is a bit messy, and you have to remember to include the param in every relevent URL.
I'd just use an object with static methods for this. The object would wrap around the session item you use to store redirect URLS.
The methods would probably be as follows (all public static):
setRedirectUrl(string URL)
doRedirect(string defaultURL)
setRedirectUrl would be called in any action that produces links / forms which need to redirect to a given url. So say you had a projects view action that generates a list of projects, each with tasks that can be performed on them (e.g. delete, edit) you would call RedirectClass.setRedirectUrl("/project/view-all") in the code for this action.
Then lets say the user clicks delete, they need to be redirected to the view page after a delete action, so in the delete action you would call RedirectClass.setRedirectUrl("/project/view-all"). This method would look to see if the redirect variable was set in the session. If so redirect to that URL. If not, redirect to the default url (the string passed to the setRedirectUrl method).
I agree with "rmbarnes.myopenid.com" regarding this issue as being platform independent.
I would store the calling page URL in the QueryString or in a hidden field (for example in ViewState for ASP.NET). If you will store it outside of the page scope (such as Session, global variable - Application State and so on) then it will not be just overkill as Tom said but it will bring you trouble.
What kind of trouble? Trouble if the user has more than one tab (window) of that browser open. The tabs (or windows) of the same browser will probably share the same session and the redirection will not be the one expected and all the user will feel is that it is a bug.
My 2 eurocents..
I personally would store the required redirection info in an object and handle globally. I would avoid using a QueryString param or the like since they could try bouncing themselves back to a page they are not supposed to (possible security issue?). You could then create a static method to handle the redirection object, which could read the information and act accordingly. This encapsulates your redirection process within one page.
Using an object also means you can later extend it if required (such as adding return messages and other info).
For example (this is a 2 minute rough guideline BTW!):
public partial class _Default : System.Web.UI.Page
{
void Redirect(string url, string messsage)
{
RedirectionParams paras = new RedirectionParams(url, messsage);
RedirectionHandler(paras); // pass to some global method (or this could BE the global method)
}
protected void Button1_Click(object sender, EventArgs e)
{
Redirect("mypage.aspx", "you have been redirected");
}
}
public class RedirectionParams
{
private string _url;
public string URL
{
get { return _url; }
set { _url = value; }
}
private string _message;
public string Message
{
get { return _message; }
set { _message = value; }
}
public RedirectionParams(string url, string message)
{
this.URL = url;
this.Message = message;
}
}

Resources