¿Se puede utilizar el enrutamiento ASP.NET para crear URL “limpias” para los manejadores .ashx (IHttpHander)?

Tengo algunos servicios REST que usan los viejos IHttpHandler s. Me gustaría generar URL más limpias, para que no tenga .ashx en la ruta. ¿Hay alguna manera de utilizar el enrutamiento ASP.NET para crear rutas que se correlacionen con manejadores de ashx? He visto este tipo de rutas anteriormente:

 // 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))); 

Intentar utilizar RouteTable.Routes.MapPageRoute() genera un error (que el controlador no deriva de la Page ). System.Web.Routing.RouteBase solo parece tener 2 clases derivadas: ServiceRoute para servicios y DynamicDataRoute para MVC. No estoy seguro de lo que hace MapPageRoute() (Reflector no muestra el cuerpo del método, solo muestra “Rendimiento crítico para incorporar este tipo de método a través de los límites de la imagen NGen”).

Veo que RouteBase no está sellado, y tiene una interfaz relativamente simple:

 public abstract RouteData GetRouteData(HttpContextBase httpContext); public abstract VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values); 

Entonces quizás pueda hacer mi propio HttpHandlerRoute. Voy a dar una oportunidad, pero si alguien sabe de una forma existente o incorporada de mapeo de rutas a IHttpHandlers, sería genial.

Ok, he estado averiguando esto desde que originalmente hice la pregunta, y finalmente tengo una solución que hace justo lo que quiero. Sin embargo, hay que dar un poco de explicación inicial. IHttpHandler es una interfaz muy básica:

 bool IsReusable { get; } void ProcessRequest(HttpContext context) 

No hay una propiedad incorporada para acceder a los datos de ruta, y los datos de ruta tampoco se pueden encontrar en el contexto o la solicitud. Un objeto System.Web.UI.Page tiene una propiedad RouteData , ServiceRoute s hace todo el trabajo de interpretar sus UriTemplates y pasa los valores al método correcto internamente, y ASP.NET MVC proporciona su propia forma de acceder a los datos de la ruta. Incluso si tenía un RouteBase que (a) determinaba si la URL entrante era compatible con su ruta y (b) analizaba la url para extraer todos los valores individuales que se utilizarían desde su IHttpHandler, no hay una manera fácil de pasar esa enrutar datos a su IHttpHandler. Si desea mantener su IHttpHandler “puro”, por así decirlo, se responsabiliza de tratar con la url y de extraer cualquier valor de la misma. La implementación de RouteBase en este caso solo se usa para determinar si tu IHttpHandler se debe usar en absoluto.

Sin embargo, queda un problema. Una vez que RouteBase determina que la URL entrante coincide con su ruta, se transfiere a un IRouteHandler, que crea las instancias de IHttpHandler que desea manejar su solicitud. Pero, una vez que está en su IHttpHandler, el valor de context.Request.CurrentExecutionFilePath es engañoso. Es la url que vino del cliente, menos la cadena de consulta. Entonces no es la ruta a su archivo .ashx. Y, cualquier parte de su ruta que sea constante (como el nombre del método) será parte de ese valor de ruta de archivo de ejecución. Esto puede ser un problema si usa UriTemplates dentro de su IHttpHandler para determinar qué método específico dentro de su IHttpHandler debe entregar la solicitud.

Ejemplo: si tenía un controlador .ashx en /myApp/services/myHelloWorldHandler.ashx Y tenía esta ruta asignada al controlador: “services / hello / {name}” Y navegaba a esta url, tratando de llamar al SayHello(string name) método de su controlador: http: // localhost / myApp / services / hello / SayHello / Sam

Entonces su CurrentExecutionFilePath sería: / myApp / services / hello / Sam. Incluye partes de la url de ruta, lo cual es un problema. Desea que la ruta del archivo de ejecución coincida con su url de ruta. Las implementaciones de RouteBase e IRouteHandler ocupan de este problema.

Antes de pegar las 2 clases, aquí hay un ejemplo de uso muy simple. Tenga en cuenta que estas implementaciones de RouteBase y IRouteHandler en realidad funcionarán para IHttpHandlers que ni siquiera tienen un archivo .ashx, que es bastante conveniente.

 // A "headless" IHttpHandler route (no .ashx file required) RouteTable.Routes.Add(new GenericHandlerRoute("services/headless")); 

Esto provocará que todas las direcciones URL entrantes que coinciden con la ruta “servicios / sin cabeza” se transfieran a una nueva instancia de HeadlessService IHttpHandler (HeadlessService es solo un ejemplo en este caso. Sería cualquier implementación de IHttpHandler a la que quisiera pasar) .

Ok, entonces aquí están las implementaciones de la clase de enrutamiento, los comentarios y todo:

 ///  /// 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. ///  ///  public class GenericHandlerRoute : 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 routeHandler = new GenericHandlerRouteHandler(); 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 : IRouteHandler where T : IHttpHandler, new() { public IHttpHandler GetHttpHandler(RequestContext requestContext) { return new T(); } } 

Sé que esta respuesta ha sido bastante larga, pero no fue un problema fácil de resolver. La lógica del núcleo era bastante fácil, el truco era hacer que tu IHttpHandler estuviera al tanto de la “url base”, de modo que pudiera determinar correctamente qué partes de la URL pertenecen a la ruta y qué partes son argumentos reales para la llamada de servicio.

Estas clases se usarán en mi próxima biblioteca C # REST, RestCake . Espero que mi camino por el hoyo de enrutamiento de enrutamiento ayude a cualquier otra persona que decida utilizar RouteBase, y haga cosas geniales con IHttpHandlers.

De hecho, me gusta más la solución de Joel, ya que no requiere que conozcas el tipo de controlador mientras intentas configurar tus rutas. Yo lo hubiera votado, pero por desgracia, no tengo la reputación requerida.

De hecho, encontré una solución que siento que es mejor que ambas. El código fuente original del que derivé mi ejemplo se puede encontrar enlazado aquí http://weblogs.asp.net/leftslipper/archive/2009/10/07/introducing-smartyroute-a-smarty-ier-way-to-do- routing-in-asp-net-applications.aspx .

Esto es menos código, tipo agnóstico y rápido.

 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; } } 

Y un áspero ejemplo de uso

 String handlerPath = "~/UploadHandler.ashx"; RouteTable.Routes.Add(new Route("files/upload", new HttpHandlerRoute(handlerPath))); 

EDITAR: Acabo de editar este código porque tuve algunos problemas con el anterior. Si está utilizando la versión anterior, actualice.

Este hilo es un poco viejo, pero acabo de volver a escribir parte del código aquí para hacer lo mismo, pero de una manera más elegante, utilizando un método de extensión.

Estoy usando esto en Webforms de ASP.net, y me gusta tener los archivos ashx en una carpeta y poder llamarlos ya sea mediante el enrutamiento o una solicitud normal.

Así que casi agarré el código de shellscape e hice un método de extensión que funciona. Al final, sentí que también debería admitir pasar el objeto IHttpHandler en lugar de su Url, así que escribí y sobrecargué el método MapHttpHandlerRoute para eso.

 namespace System.Web.Routing { public class HttpHandlerRoute : IRouteHandler where T: IHttpHandler { private String _virtualPath = null; public HttpHandlerRoute(String virtualPath) { _virtualPath = virtualPath; } public HttpHandlerRoute() { } public IHttpHandler GetHttpHandler(RequestContext requestContext) { return Activator.CreateInstance(); } } 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.Comstacktion.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(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()); routes.Add(routeName, route); } } } 

Lo estoy colocando dentro del mismo espacio de nombres de todos los objetos de enrutamiento nativos, por lo que estará disponible automáticamente.

Para usar esto solo tienes que llamar:

 // using the handler url routes.MapHttpHandlerRoute("DoSomething", "Handlers/DoSomething", "~/DoSomething.ashx"); 

O

 // using the type of the handler routes.MapHttpHandlerRoute("DoSomething", "Handlers/DoSomething"); 

Disfruta, Alex

Sí, me di cuenta de eso también. Quizás haya una forma incorporada de ASP.NET para hacer esto, pero el truco para mí fue crear una nueva clase derivada de 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."); } } } } 

Para crear una ruta para .ashx:

 IRouteHandler routeHandler = new GenericHandlerRouteHandler("~/somehandler.ashx"); Route route = new Route("myroute", null, null, null, routeHandler); RouteTable.Routes.Add(route); 

El código anterior puede necesitar ser mejorado para trabajar con sus argumentos de ruta, pero es el punto de partida. Comentarios bienvenidos.

Todas estas respuestas son muy buenas. Me encanta la simplicidad de la GenericHandlerRouteHandler del Sr. Meacham. Es una gran idea eliminar una referencia innecesaria a una ruta virtual si conoce la clase específica HttpHandler . Sin GenericHandlerRoute clase GenericHandlerRoute no es necesaria. La clase Route existente que deriva de RouteBase ya maneja toda la complejidad de la coincidencia de ruta, parámetros, etc., así que podemos usarlo junto con GenericHandlerRouteHandler .

A continuación se muestra una versión combinada con un ejemplo de uso de la vida real que incluye parámetros de ruta.

Primero son los manejadores de ruta. Hay dos incluidos, aquí, ambos con el mismo nombre de clase, pero uno que es genérico y usa información de tipo para crear una instancia del HttpHandler específico como en el uso del Sr. Meacham, y uno que utiliza una ruta virtual y BuildManager para crear una instancia del HttpHandler apropiado como en el uso de shellscape. La buena noticia es que .NET permite que ambos vivan juntos, así que podemos usar el que queramos y podemos cambiar entre ellos como lo deseemos.

 using System.Web; using System.Web.Comstacktion; using System.Web.Routing; public class HttpHandlerRouteHandler : 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)); } } 

Supongamos que creamos un HttpHandler que transmite documentos a los usuarios desde un recurso fuera de nuestra carpeta virtual, incluso desde una base de datos, y que queremos engañar al navegador del usuario para que crea que estamos sirviendo directamente un archivo específico en lugar de simplemente proporcionar un descargar (es decir, permitir que los complementos del navegador manejen el archivo en lugar de forzar al usuario a guardar el archivo). El HttpHandler puede esperar una identificación de documento con la que ubicar el documento para proporcionar, y puede esperar un nombre de archivo para proporcionar al navegador, uno que puede diferir del nombre de archivo utilizado en el servidor.

A continuación, se muestra el registro de la ruta utilizada para lograr esto con DocumentHandler HttpHandler :

 routes.Add("Document", new Route("document/{documentId}/{*fileName}", new HttpHandlerRouteHandler())); 

{*fileName} lugar de solo {fileName} para permitir que el parámetro fileName actúe como un parámetro catch-all opcional.

Para crear una URL para un archivo servido por este HttpHandler , podemos agregar el siguiente método estático a una clase donde dicho método sería apropiado, como en la clase HttpHandler :

 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; } 

MimeMap las definiciones de MimeMap e IsPassThruMimeType para mantener este ejemplo simple. Pero estos están destinados a determinar si los tipos de archivos específicos deben proporcionar sus nombres de archivo directamente en la URL, o más bien en un encabezado HTTP Content-Disposition . Algunas extensiones de archivos podrían ser bloqueadas por IIS o URL Scan, o podrían causar la ejecución de código que podría causar problemas para los usuarios, especialmente si el origen del archivo es otro usuario que es malicioso. Puede reemplazar esta lógica con otra lógica de filtrado u omitir dicha lógica por completo si no está expuesto a este tipo de riesgo.

Dado que en este ejemplo particular, el nombre del archivo puede omitirse de la URL, entonces, obviamente, debemos recuperar el nombre del archivo de alguna parte. En este ejemplo en particular, el nombre del archivo se puede recuperar realizando una búsqueda utilizando la identificación del documento, e incluir un nombre de archivo en la URL tiene como único objective mejorar la experiencia del usuario. Por lo tanto, DocumentHandler HttpHandler puede determinar si se proporcionó un nombre de archivo en la URL, y si no lo fue, entonces simplemente puede agregar un encabezado HTTP Content-Disposition a la respuesta.

Siguiendo con el tema , la parte importante del bloque de código anterior es el uso de RouteTable.Routes.GetVirtualPath() y los parámetros de enrutamiento para generar un URL del objeto Route que creamos durante el proceso de registro de ruta.

Aquí hay una versión HttpHandler clase DocumentHandler HttpHandler (se omite por motivos de claridad). Puede ver que esta clase usa parámetros de ruta para recuperar la identificación del documento y el nombre del archivo cuando puede; de lo contrario, intentará recuperar el ID del documento a partir de un parámetro de cadena de consulta (es decir, suponiendo que no se utilizó el enrutamiento).

 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. } } 

Este ejemplo utiliza algunas clases personalizadas adicionales, como una clase de Utility para simplificar algunas tareas triviales. Pero espero que puedas superar eso. La única parte realmente importante en esta clase con respecto al tema actual, por supuesto, es la recuperación de los parámetros de ruta desde context.Request.RequestContext.RouteData . Pero he visto varias publicaciones en otros lugares preguntando cómo transmitir archivos de gran tamaño usando un HttpHandler sin masticar la memoria del servidor, por lo que me pareció una buena idea combinar ejemplos.