ASP.NET Routing可用于为.ashx(IHttpHander)处理程序创建“干净”的URL吗?

时间:2010-07-29 05:58:52

标签: asp.net ihttphandler asp.net-routing

我使用普通的IHttpHandler来提供一些REST服务。我想生成更干净的URL,以便我在路径中没有.ashx。有没有办法使用ASP.NET路由创建映射到ashx处理程序的路由?我之前见过这些类型的路线:

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

尝试使用RouteTable.Routes.MapPageRoute()会产生错误(处理程序不会从Page派生)。 System.Web.Routing.RouteBase似乎只有两个派生类:ServiceRoute用于服务,DynamicDataRoute用于MVC。我不确定MapPageRoute()是做什么的(Reflector没有显示方法体,它只是显示“对于跨越NGen图像边界内联这种方法的性能至关重要”)。

我看到RouteBase没有密封,并且界面相对简单:

public abstract RouteData GetRouteData(HttpContextBase httpContext);

public abstract VirtualPathData GetVirtualPath(RequestContext requestContext,
    RouteValueDictionary values);

所以也许我可以制作自己的HttpHandlerRoute。我会给出一个镜头,但如果有人知道映射到IHttpHandlers的路由的现有或内置方式,那将是伟大的。

5 个答案:

答案 0 :(得分:26)

好吧,自从我最初提出这个问题以来,我一直在想这个问题,我终于找到了一个可以满足我想要的解决方案。然而,一些前期解释是应该的。 IHttpHandler是一个非常基本的界面:

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

没有用于访问路径数据的内置属性,也无法在上下文或请求中找到路径数据。 System.Web.UI.Page对象具有RouteData属性,ServiceRoute执行解释UriTemplates并将值传递给内部正确方法的所有工作,ASP.NET MVC提供了自己的方法访问路线数据。即使您有一个RouteBase(a)确定传入的URL是否与您的路由匹配,并且(b)解析了url以从IHttpHandler中提取所有要使用的单个值,这并不容易将路由数据传递给IHttpHandler的方法。如果你想保持你的IHttpHandler“纯粹”,可以说,它负责处理url,以及如何从中提取任何值。在这种情况下,RouteBase实现仅用于确定是否应该使用IHttpHandler。

然而,仍有一个问题。一旦RouteBase确定传入的URL与您的路由匹配,它就会传递给IRouteHandler,它会创建您想要处理请求的IHttpHandler的实例。但是,一旦你进入你的IHttpHandler,context.Request.CurrentExecutionFilePath的价值就会产生误导。这是来自客户端的URL,减去查询字符串。所以它不是你的.ashx文件的路径。并且,路由中任何常量的部分(例如方法的名称)都将是该执行文件路径值的一部分。如果您在IHttpHandler中使用UriTemplates来确定IHttpHandler中的哪个特定方法应该处理请求,那么这可能是个问题。

示例:如果/myApp/services/myHelloWorldHandler.ashx上有.ashx处理程序 你有这个映射到处理程序的路由:“services / hello / {name}” 您导航到此网址,尝试调用处理程序的SayHello(string name)方法: http://localhost/myApp/services/hello/SayHello/Sam

然后你的CurrentExecutionFilePath将是:/ myApp / services / hello / Sam。它包括路由URL的部分,这是一个问题。您希望执行文件路径与您的路由URL匹配。 RouteBaseIRouteHandler的以下实现处理此问题。

在粘贴2个类之前,这是一个非常简单的用法示例。请注意,RouteBase和IRouteHandler的这些实现实际上适用于甚至没有.ashx文件的IHttpHandler,这非常方便。

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

这将导致所有与“services / headless”路由匹配的传入url被移交给HeadlessService IHttpHandler的新实例(HeadlessService在这种情况下只是一个例子。它将是IHttpHandler实现的任何内容你想传递给。)。

好的,这里是路由类实现,注释和所有:

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

我知道这个答案已经很长,但这不是一个容易解决的问题。核心逻辑很简单,诀窍是以某种方式让你的IHttpHandler知道“基本URL”,这样它就可以正确地确定url的哪些部分属于路由,以及哪些部分是服务调用的实际参数。

这些类将在我即将推出的C#REST库RestCake中使用。我希望我在路由兔子洞中的路径能够帮助任何决定使用RouteBase的人,并使用IHttpHandlers做很酷的事情。

答案 1 :(得分:13)

我实际上更喜欢Joel的解决方案,因为当您尝试设置路线时,它不需要您知道处理程序的类型。我赞成它,但唉,我没有必要的声誉。

我实际上找到了一种解决方案,我觉得它比上面提到的要好。我从我的示例中导出的原始源代码可以在此处链接http://weblogs.asp.net/leftslipper/archive/2009/10/07/introducing-smartyroute-a-smarty-ier-way-to-do-routing-in-asp-net-applications.aspx

代码少,类型不可知,速度快。

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

使用的粗略例子

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

答案 2 :(得分:9)

编辑:我刚编辑了这段代码,因为我遇到了旧代码的一些问题。如果您使用的是旧版本,请更新。

这个帖子有点陈旧但是我只是在这里重写了一些代码来做同样的事情,但是以更优雅的方式,使用扩展方法。

我在ASP.net Webforms上使用它,我喜欢将ashx文件放在一个文件夹中,并且可以使用路由或普通请求来调用它们。

所以我几乎抓住了shellscape的代码并制作了一个扩展方法。最后我觉得我也应该支持传递IHttpHandler对象而不是它的Url,所以我为此编写并重载了MapHttpHandlerRoute方法。

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

我将它放在所有本地路由对象的相同名称空间中,因此它将自动可用。

所以要使用它,你只需要打电话:

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

或者

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

享受, 亚历

答案 3 :(得分:5)

所有这些答案都非常好。我喜欢Meacham先生的GenericHandlerRouteHandler<T>课程的简单性。如果您知道特定的HttpHandler类,那么消除对虚拟路径的不必要引用是个好主意。但是,不需要GenericHandlerRoute<T>类。从Route派生的现有RouteBase类已经处理了路由匹配,参数等的所有复杂性,因此我们可以将它与GenericHandlerRouteHandler<T>一起使用。

以下是具有实际使用示例的组合版本,其中包括路线参数。

首先是路线处理程序。这里包含两个 - 两个都具有相同的类名,但是一个是通用的,并且使用类型信息来创建特定HttpHandler的实例,如Meacham先生的用法,以及使用虚拟路径的实例和BuildManager一样,在shellscape的用法中创建适当的HttpHandler的实例。好消息是.NET允许两者并排生活,所以我们可以使用我们想要的任何一种,并且可以按照我们的意愿在它们之间切换。

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

}

假设我们创建了一个HttpHandler,它将文档从我们虚拟文件夹之外的资源流式传输给用户,甚至可能来自数据库,我们想让用户的浏览器误认为我们直接服务于特定文件而不是简单地提供下载(即允许浏览器的插件处理文件而不是强制用户保存文件)。 HttpHandler可能需要一个文档ID来定位要提供的文档,并且可能期望向浏览器提供文件名 - 可能与服务器上使用的文件名不同。

以下显示使用DocumentHandler HttpHandler注册用于完成此操作的路线:

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

我使用{*fileName}而非{fileName}来允许fileName参数充当可选的catch-all参数。

要为此HttpHandler提供的文件创建URL,我们可以将以下静态方法添加到适合此类方法的类中,例如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;
}

我省略了MimeMapIsPassThruMimeType的定义,以保持此示例的简单性。但这些用于确定特定文件类型是否应直接在URL中提供其文件名,或者更确切地说是Content-Disposition HTTP标头中。某些文件扩展名可能被IIS或URL扫描阻止,或者可能导致代码执行,这可能会导致用户出现问题 - 尤其是如果文件的来源是另一个恶意用户。您可以使用其他过滤逻辑替换此逻辑,或者如果您没有遇到此类风险,则完全省略此逻辑。

由于在这个特定的例子中,文件名可能会从URL中省略,显然,我们必须从某个地方检索文件名。在该特定示例中,可以通过使用文档id执行查找来检索文件名,并且在URL中包括文件名仅旨在改善用户的体验。因此,DocumentHandler HttpHandler可以确定URL中是否提供了文件名,如果不是,则可以只为响应添加Content-Disposition HTTP标头。

坚持主题,上述代码块的重要部分是使用RouteTable.Routes.GetVirtualPath()和路由参数从我们创建的Route对象生成网址在路线注册过程中。

这是DocumentHandler HttpHandler类的淡化版本(为了清晰起见,省略了很多)。您可以看到此类使用路由参数来检索文档ID和文件名(如果可以);否则,它将尝试从查询字符串参数中检索文档id(即,假设未使用路由)。

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

此示例使用一些其他自定义类,例如Utility类来简化一些简单的任务。但希望你可以解决这个问题。当然,关于当前主题,本课程中唯一非常重要的部分是从context.Request.RequestContext.RouteData检索路由参数。但是我在其他地方看到过几个帖子,询问如何使用HttpHandler来传输大文件而不会占用服务器内存,所以结合示例似乎是个好主意。

答案 4 :(得分:4)

是的,我也注意到了。也许有一种内置的ASP.NET方法可以做到这一点,但我的诀窍是创建一个派生自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.");
            }
        }
    }
}

为.ashx创建路线:

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

上面的代码可能需要增强才能使用你的路由参数,但它是起点。欢迎评论。

相关问题