所有这些答案都很好。我喜欢 Meacham 先生的 GenericHandlerRouteHandler<T> 课程的简单性。如果您知道特定的 HttpHandler 类,那么消除对虚拟路径的不必要引用是一个好主意。但是,不需要 GenericHandlerRoute<T> 类。现有的从RouteBase 派生的Route 类已经处理了路由匹配、参数等的所有复杂性,因此我们可以将它与GenericHandlerRouteHandler<T> 一起使用。
下面是一个包含路由参数的真实使用示例的组合版本。
首先是路由处理程序。这里包括了两个——都具有相同的类名,但一个是通用的并使用类型信息来创建特定 HttpHandler 的实例,如 Meacham 先生的用法,另一个使用虚拟路径和 @ 987654328@ 创建适当的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 参数充当可选的包罗万象的参数。
要为此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;
}
我省略了MimeMap 和IsPassThruMimeType 的定义,以保持这个示例简单。但这些旨在确定特定文件类型是否应直接在 URL 中提供其文件名,或者更确切地说是在 Content-Disposition HTTP 标头中提供它们的文件名。某些文件扩展名可能会被 IIS 或 URL 扫描阻止,或者可能导致代码执行,这可能会给用户带来问题——尤其是当文件的来源是另一个恶意用户时。您可以用其他一些过滤逻辑替换此逻辑,或者如果您没有面临此类风险,则完全省略此类逻辑。
由于在这个特定的示例中,URL 中可能会省略文件名,因此很明显,我们必须从某个地方检索文件名。在此特定示例中,可以通过使用文档 ID 执行查找来检索文件名,并且在 URL 中包含文件名仅旨在改善用户体验。因此,DocumentHandlerHttpHandler 可以确定 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 流式传输大文件而不占用服务器内存,因此结合示例似乎是个好主意。