所有这些答案都非常好。我喜欢 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;
}
我省略了 和 的定义,MimeMap
以IsPassThruMimeType
保持这个例子简单。但这些旨在确定特定文件类型是否应直接在 URL 中或在Content-Disposition
HTTP 标头中提供其文件名。某些文件扩展名可能会被 IIS 或 URL 扫描阻止,或者可能导致代码执行,这可能会给用户带来问题——尤其是当文件的来源是另一个恶意用户时。您可以用其他一些过滤逻辑替换此逻辑,或者如果您没有面临此类风险,则完全省略此类逻辑。
由于在这个特定示例中,URL 中可能会省略文件名,因此很明显,我们必须从某个地方检索文件名。在此特定示例中,可以通过使用文档 ID 执行查找来检索文件名,并且在 URL 中包含文件名仅旨在改善用户体验。因此,DocumentHandler
HttpHandler
可以确定 URL 中是否提供了文件名,如果没有,则可以简单地将Content-Disposition
HTTP 标头添加到响应中。
停留在主题上,上述代码块的重要部分是使用和路由参数从我们在路由注册过程中创建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
而不占用服务器内存,因此结合示例似乎是个好主意。