我有一些使用普通旧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
似乎只有2个派生类:ServiceRoute
用于服务,DynamicDataRoute
用于MVC.我不确定是什么MapPageRoute()
(Reflector没有显示方法体,它只显示"在NGen图像边界内嵌这种方法的性能至关重要").
我看到它RouteBase
没有密封,并且有一个相对简单的界面:
public abstract RouteData GetRouteData(HttpContextBase httpContext); public abstract VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values);
所以也许我可以制作自己的HttpHandlerRoute.我会给出一个镜头,但如果有人知道现有的或内置的映射路由到IHttpHandlers的方式,这将是伟大的.
好吧,自从我最初提出这个问题以来,我一直在想这个问题,我终于找到了一个可以满足我想要的解决方案.然而,一些前期解释是应该的.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}"并且您导航到此URL,尝试调用该SayHello(string name)
方法您的处理程序:
http:// localhost/myApp/services/hello/SayHello/Sam
那你的意思CurrentExecutionFilePath
是:/ myApp/services/hello/Sam.它包括路由URL的部分,这是一个问题.您希望执行文件路径与您的路由URL匹配.下面的实现RouteBase
和IRouteHandler
处理这个问题.
在我粘贴2个类之前,这是一个非常简单的用法示例.请注意,RouteBase和IRouteHandler的这些实现实际上适用于甚至没有.ashx文件的IHttpHandler,这非常方便.
// A "headless" IHttpHandler route (no .ashx file required) RouteTable.Routes.Add(new GenericHandlerRoute("services/headless"));
这将导致所有与"服务/无头"路由匹配的传入URL被切换到HeadlessService
IHttpHandler 的新实例(在这种情况下,HeadlessService只是一个示例.它将是您希望传递给IHttpHandler的任何实现).
好的,所以这里是路由类实现,注释和所有:
////// 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(); } }
我知道这个答案已经很长时间了,但要解决这个问题并不容易.核心逻辑很简单,诀窍是以某种方式让你的IHttpHandler知道"基本URL",这样它就可以正确地确定url的哪些部分属于路由,以及哪些部分是服务调用的实际参数.
这些类将在我即将推出的C#REST库RestCake中使用.我希望我在路由兔子洞的路径将帮助其他决定使用RouteBase的人,并使用IHttpHandlers做很酷的事情.
我实际上更喜欢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)));
这个线程有点旧,但我只是在这里重新编写了一些代码来做同样的事情,但是以更优雅的方式,使用扩展方法.
我在ASP.net Webforms上使用它,我喜欢将ashx文件放在一个文件夹上,并且可以使用路由或普通请求来调用它们.
所以我几乎抓住了shellscape的代码并制作了一个扩展方法来完成这个工作.最后我觉得我也应该支持传递IHttpHandler对象而不是它的Url,所以我为此编写并重载了MapHttpHandlerRoute方法.
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.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 (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); } } }
我将它放在所有本地路由对象的相同名称空间中,因此它将自动可用.
所以要使用它你只需要打电话:
// using the handler url routes.MapHttpHandlerRoute("DoSomething", "Handlers/DoSomething", "~/DoSomething.ashx");
要么
// using the type of the handler routes.MapHttpHandlerRoute("DoSomething", "Handlers/DoSomething");
享受,亚历克斯
所有这些答案都很好。我喜欢Meacham先生GenericHandlerRouteHandler
课堂的简单性。如果您知道特定的HttpHandler
类,则消除不必要的虚拟路径引用是一个好主意。该GenericHandlerRoute
但是不需要类。Route
从中派生的现有类RouteBase
已经处理了路由匹配,参数等的所有复杂性,因此我们可以将其与一起使用GenericHandlerRouteHandler
。
以下是结合了实际使用示例的组合版本,其中包括路由参数。
首先是路由处理程序。其中包括两个-两者都具有相同的类名,但是一个是通用的并且使用类型信息来创建特定实例(HttpHandler
如Meacham先生所使用的实例),另一个是使用虚拟路径并BuildManager
创建实例。与HttpHandler
shellscape的用法相同。好消息是.NET允许两者并存,所以我们可以使用任何我们想要的东西,并且可以根据需要在它们之间切换。
using System.Web; using System.Web.Compilation; 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)); } }
假设我们创建了一个HttpHandler
,可以从虚拟文件夹之外的资源(甚至从数据库)向用户流式传输文档,并且我们想欺骗用户的浏览器,使他们认为我们直接在提供特定文件,而不是简单地提供下载(即,允许浏览器的插件处理文件,而不是强迫用户保存文件)。在HttpHandler
可预期的文件ID,以找到该文件提供,并可以期待文件名提供给浏览器-一个可以从服务器上使用的文件名不同。
以下显示了通过来完成此操作的路线的注册DocumentHandler
HttpHandler
:
routes.Add("Document", new Route("document/{documentId}/{*fileName}", new HttpHandlerRouteHandler()));
我使用{*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; }
为了使此示例简单,我省略了MimeMap
和的定义IsPassThruMimeType
。但是,这些命令旨在确定特定文件类型是否应直接在URL中或在Content-Disposition
HTTP标头中提供其文件名。某些文件扩展名可能被IIS或URL扫描阻止,或者可能导致执行代码,可能给用户带来麻烦-尤其是在文件源是另一个恶意用户的情况下。您可以使用其他一些过滤逻辑来代替此逻辑,或者,如果您不承担此类风险,则可以完全省略此类逻辑。
由于在此特定示例中,URL中可能省略了文件名,因此,显然,我们必须从某个位置检索文件名。在此特定示例中,可以通过使用文档ID执行查找来检索文件名,并且在URL中包含文件名仅是为了改善用户体验。因此,DocumentHandler
HttpHandler
可以确定URL中是否提供了文件名,如果没有,则可以简单地将Content-Disposition
HTTP标头添加到响应中。
继续讨论主题,上面代码块的重要部分是使用RouteTable.Routes.GetVirtualPath()
和路由参数从Route
我们在路由注册过程中创建的对象生成URL 。
这是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
不占用服务器内存的情况下使用流式传输大文件,因此组合示例似乎是个好主意。