¿Se puede usar el enrutamiento de ASP.NET para crear direcciones URL "limpias" para los controladores.ashx(IHttpHander)?
ihttphandler asp.net-routing (5)
EDIT: Acabo de editar este código porque tuve algunos problemas con el anterior. Si está utilizando la versión anterior, por favor actualice.
Este hilo es un poco viejo pero acabo de reescribir algo del código aquí para hacer lo mismo pero de una manera más elegante, usando un método de extensión.
Estoy usando esto en los formularios web de ASP.net, y me gusta tener los archivos ashx en una carpeta y poder llamarlos usando enrutamiento o una solicitud normal.
Así que prácticamente agarré el código de Shellscape e hice un método de extensión que hace el truco. Al final sentí que también debería admitir pasar el objeto IHttpHandler en lugar de su URL, por lo que escribí y sobrecargué el método MapHttpHandlerRoute para eso.
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);
}
}
}
Lo estoy colocando dentro del mismo espacio de nombres de todos los objetos de enrutamiento nativos para que esté disponible automáticamente.
Así que 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<MyHttpHanler>("DoSomething", "Handlers/DoSomething");
Disfruta, alex
Tengo algunos servicios REST que utilizan los antiguos IHttpHandler
s. Me gustaría generar URL más limpias, para que no tenga el .ashx en la ruta. ¿Hay alguna forma de usar el enrutamiento ASP.NET para crear rutas que se asignen a los manejadores 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)));
Al intentar usar RouteTable.Routes.MapPageRoute()
genera un error (que el controlador no deriva de 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 integrar 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);
Así que tal vez pueda hacer mi propio HttpHandlerRoute. Le daré una oportunidad, pero si alguien sabe de una manera existente o integrada de mapear rutas a IHttpHandlers, sería genial.
De hecho, me gusta más la solución de Joel, ya que no es necesario que conozca el tipo de manejador mientras intenta configurar sus rutas. Lo promocionaría, pero, por desgracia, no tengo la reputación requerida.
De hecho, encontré una solución que creo que es mejor que las dos mencionadas. El código fuente original del que obtuve mi ejemplo se puede encontrar 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 ejemplo aproximado de uso.
String handlerPath = "~/UploadHandler.ashx";
RouteTable.Routes.Add(new Route("files/upload", new HttpHandlerRoute(handlerPath)));
Ok, he estado resolviendo esto desde que originalmente hice la pregunta, y finalmente tengo una solución que hace exactamente lo que quiero. Sin embargo, se debe un poco de explicación por adelantado. IHttpHandler es una interfaz muy básica:
bool IsReusable { get; }
void ProcessRequest(HttpContext context)
No hay una propiedad integrada 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 pasar los valores al método correcto internamente, y ASP.NET MVC proporciona su propia forma de acceder a los datos de ruta. Incluso si tenía una RouteBase
que (a) determinó si la url entrante era una coincidencia para su ruta y (b) analizó la url para extraer todos los valores individuales que se utilizarán desde su IHttpHandler, no hay manera fácil de pasar eso direccionar datos a su IHttpHandler. Si quieres mantener tu IHttpHandler "puro", por así decirlo, asume la responsabilidad de tratar con la URL y cómo extraer cualquier valor de ella. La implementación de RouteBase en este caso solo se usa para determinar si su IHttpHandler debe usarse en absoluto.
Sin embargo, queda un problema. Una vez que la RouteBase determina que la URL entrante es una coincidencia para su ruta, pasa a un IRouteHandler, que crea las instancias del IHttpHandler que desea manejar su solicitud. Pero, una vez que estás en tu IHttpHandler, el valor de context.Request.CurrentExecutionFilePath
es engañoso. Es la url que viene del cliente, menos la cadena de consulta. Así que no es la ruta a su archivo .ashx. Y, cualquier parte de su ruta que sea constante (como el nombre del método) formará parte del valor de la ruta del archivo de ejecución. Esto puede ser un problema si utiliza 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 que se asignaba al controlador: "services / hello / {name}" Y navegó a esta url, tratando de llamar a SayHello(string name)
Método SayHello(string name)
de su controlador: http://localhost/myApp/services/hello/SayHello/Sam
Entonces su CurrentExecutionFilePath
sería: / myApp / services / hello / Sam. Incluye partes de la ruta url, que es un problema. Desea que la ruta del archivo de ejecución coincida con su URL de ruta. Las siguientes implementaciones de RouteBase
y IRouteHandler
tratan 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 realmente funcionarán para IHttpHandlers que ni siquiera tienen un archivo .ashx, lo cual es bastante conveniente.
// A "headless" IHttpHandler route (no .ashx file required)
RouteTable.Routes.Add(new GenericHandlerRoute<HeadlessService>("services/headless"));
Eso hará que todas las direcciones URL entrantes que coincidan con la ruta "services / headless" 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, aquí están las implementaciones de clase de enrutamiento, comentarios y todo:
/// <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();
}
}
Sé que esta respuesta ha sido bastante larga, pero no fue un problema fácil de resolver. La lógica central era bastante fácil, el truco consistía en hacer que su IHttpHandler fuera consciente de la "base url", 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 utilizarán en mi próxima biblioteca de C # REST, RestCake . Espero que mi camino por el agujero del conejo de enrutamiento ayude a cualquier otra persona que decida a RouteBase y haga cosas interesantes con IHttpHandlers.
Sí, también me di cuenta de eso. Tal vez haya una forma integrada 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 un .ashx:
IRouteHandler routeHandler = new GenericHandlerRouteHandler("~/somehandler.ashx");
Route route = new Route("myroute", null, null, null, routeHandler);
RouteTable.Routes.Add(route);
Es posible que el código anterior deba mejorarse para trabajar con los argumentos de su ruta, pero es el punto de partida. Comentarios bienvenidos.
Todas estas respuestas son muy buenas. Me encanta la simplicidad de la GenericHandlerRouteHandler<T>
de Mr. Meacham. Es una buena idea eliminar una referencia innecesaria a una ruta virtual si conoce la clase específica de HttpHandler
. Sin GenericHandlerRoute<T>
clase GenericHandlerRoute<T>
no es necesaria. La clase de Route
existente que se deriva de RouteBase
ya maneja toda la complejidad de la coincidencia de ruta, los parámetros, etc., por lo que podemos usarla junto con GenericHandlerRouteHandler<T>
.
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 están los manejadores de ruta. Aquí se incluyen dos: 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 usa una ruta de acceso 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 uno al lado del otro, así que podemos usar lo que queramos y cambiar entre ellos como deseamos.
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));
}
}
Supongamos que creamos un HttpHandler
que transmite documentos a usuarios desde un recurso fuera de nuestra carpeta virtual, tal vez 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 obligar 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 que un nombre de archivo se proporcione 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 un DocumentHandler
HttpHandler
:
routes.Add("Document", new Route("document/{documentId}/{*fileName}", new HttpHandlerRouteHandler<DocumentHandler>()));
Utilicé {*fileName}
lugar de {fileName}
para permitir que el parámetro fileName
actúe como un parámetro opcional para todos.
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 HttpHandler
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
y IsPassThruMimeType
para mantener este ejemplo simple. Pero estos están destinados a determinar si tipos de archivos específicos deben proporcionar sus nombres de archivo directamente en la URL, o más bien en un encabezado HTTP de Content-Disposition
. Algunas extensiones de archivo podrían ser bloqueadas por IIS o URL Scan, o podrían causar la ejecución de código que podría causar problemas a los usuarios, especialmente si la fuente del archivo es otro usuario malicioso. Podría reemplazar esta lógica con alguna otra lógica de filtrado u omitir dicha lógica por completo si no está expuesto a este tipo de riesgo.
Como en este ejemplo en 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 con la identificación del documento, e incluir un nombre de archivo en la URL está destinado únicamente a 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, simplemente puede agregar un encabezado HTTP de 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 una URL desde el objeto Route
que creamos durante el proceso de registro de la ruta.
Aquí hay una versión HttpHandler
clase DocumentHandler
HttpHandler
(se omite mucho por razones de claridad). Puede ver que esta clase utiliza parámetros de ruta para recuperar la identificación del documento y el nombre del archivo cuando puede; de lo contrario, intentará recuperar la identificación del documento desde 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 eliminar 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 la ruta desde context.Request.RequestContext.RouteData
. Pero he visto varias publicaciones en otros lugares que preguntan cómo transmitir archivos de gran tamaño con un HttpHandler
sin masticar la memoria del servidor, por lo que me pareció una buena idea combinar ejemplos.