28

我有一些使用普通旧IHttpHandlers 的 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似乎只有 2 个派生类:ServiceRoute用于服务和DynamicDataRoute用于 MVC。我不确定是什么MapPageRoute()(Reflector 不显示方法主体,它只显示“跨 NGen 图像边界内联此类方法的性能至关重要”)。

我看到RouteBase不是密封的,并且有一个比较简单的界面:

public abstract RouteData GetRouteData(HttpContextBase httpContext);

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

所以也许我可以制作自己的 HttpHandlerRoute。我会试一试,但如果有人知道将路由映射到 IHttpHandlers 的现有或内置方式,那就太好了。

4

5 回答 5

27

好的,自从我最初提出这个问题以来,我就一直在解决这个问题,我终于有了一个可以满足我需求的解决方案。但是,需要进行一些预先解释。IHttpHandler 是一个非常基础的接口:

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

没有用于访问路由数据的内置属性,并且在上下文或请求中也找不到路由数据。一个System.Web.UI.Page对象有一个RouteData属性,ServiceRoute它负责解释 UriTemplate 并将值传递给内部正确的方法的所有工作,并且 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}”并且您导航到此 url,试图调用该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"));

这将导致与“服务/无头”路由匹配的所有传入 url 被移交给HeadlessServiceIHttpHandler 的新实例(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 做一些很酷的事情的人。

于 2010-08-01T00:06:53.997 回答
13

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

我实际上找到了一个我觉得比上面提到的更好的解决方案。我从中获得示例的原始源代码可以在这里找到链接http://weblogs.asp.net/leftslipper/archive/2009/10/07/introducing-smartyroute-a-smarty-ier-way-to-do-路由-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)));
于 2010-08-11T04:25:16.373 回答
12

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

这个线程有点老了,但我只是在这里重新编写了一些代码来做同样的事情,但是以一种更优雅的方式,使用扩展方法。

我在 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");

享受,亚历克斯

于 2011-10-20T23:49:58.227 回答
5

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

下面是一个包含路由参数的真实使用示例的组合版本。

首先是路由处理程序。这里包括了两个 - 两个具有相同的类名,但一个是通用的并使用类型信息来创建特定的实例,HttpHandler如 Meacham 先生的用法,另一个使用虚拟路径并BuildManager创建一个实例适当HttpHandler的,如 shellscape 的用法。好消息是 .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参数充当可选的包罗万象的参数。

要为 this 提供的文件创建 URL HttpHandler,我们可以将以下静态方法添加到适合此类方法的类中,例如在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-DispositionHTTP 标头中提供其文件名。某些文件扩展名可能会被 IIS 或 URL 扫描阻止,或者可能导致代码执行,这可能会给用户带来问题——尤其是当文件的来源是另一个恶意用户时。您可以用其他一些过滤逻辑替换此逻辑,或者如果您没有面临此类风险,则完全省略此类逻辑。

由于在这个特定示例中,URL 中可能会省略文件名,因此很明显,我们必须从某个地方检索文件名。在此特定示例中,可以通过使用文档 ID 执行查找来检索文件名,并且在 URL 中包含文件名仅旨在改善用户体验。因此,DocumentHandler HttpHandler可以确定 URL 中是否提供了文件名,如果没有,则可以简单地将Content-DispositionHTTP 标头添加到响应中。

停留在主题上,上述代码块的重要部分是使用和路由参数从我们在路由注册过程中创建RouteTable.Routes.GetVirtualPath()的对象生成 URL 。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而不占用服务器内存,因此结合示例似乎是个好主意。

于 2010-12-04T09:53:47.433 回答
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);

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

于 2010-07-30T15:26:32.613 回答