资讯动态

ASP.NET Core MVC 文件上传下载实战:解析 FilesWebSite 测试站点与 FileResult 完整用法

发布时间:2026/9/10 7:22:06 来源:尧图企业网站定制
ASP.NET Core MVC 文件上传下载实战解析 FilesWebSite 测试站点与 FileResult 完整用法【免费下载链接】aspnetcoreASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.项目地址: https://gitcode.com/GitHub_Trending/as/aspnetcore本篇指南以 ASP.NET Core 官方仓库aspnetcore中的 FilesWebSite 测试站点 为线索系统讲解 MVC 中通过控制器File方法返回FileContentResult、编写注册自定义文件发送中间件以及IFormFile文件上传模型的完整用法。读完本文你将掌握PhysicalFile/File/VirtualFileResult的选型、LastModified、EntityTag、EnableRangeProcessing等 HTTP 缓存与断点续传参数的底层原理并看到这些能力在官方功能测试中如何被逐条验证。一、FilesWebSite一个专为文件收发而生的测试站点FilesWebSite 是 ASP.NET Core MVC 功能测试Functional Tests专用站点其 readme 开宗明义地说明了它的使命该站点用于演示如何通过控制器上的File方法使用FileContentResult同时演示如何编写并注册一个自定义的FileSender中间件。也就是说这个站点不是给用户演示用的 Sample而是MVC 文件结果File Result与文件上传链路的功能验证靶场。它的两个主题分别是文件下载以FileContentResult为代表的多种文件结果类型覆盖磁盘文件、流、字节数组、内嵌资源等不同数据源文件上传通过模型绑定把multipart/form-data中的文件绑定到IFormFile并读取其内容。站点采用经典的宿主搭建方式入口在 Startup.csConfigureServices中通过services.AddControllers().AddNewtonsoftJson()注册控制器与 JSON 序列化Configure中使用UseRouting()MapDefaultControllerRoute()建立默认路由Main中通过HostBuilder显式指定 Kestrel 与 IIS Integration。站点工程文件 FilesWebSite.csproj 则揭示了它的两个关键资源声明ItemGroup EmbeddedResource IncludeEmbeddedResources\** / Content Includesample.txt CopyToPublishDirectoryPreserveNewest / /ItemGroupEmbeddedResources\**被编译为程序集内嵌资源供EmbeddedFileProvider读取sample.txt作为站点内容文件随发布输出保留供PhysicalFile从磁盘读取。二、文件下载四件套FileResult 家族与 File 方法FileResult是 MVC 中所有“把文件写进响应体”的结果类型的抽象基类位于 FileResult.cs。它定义了几个贯穿所有文件结果的核心属性属性类型作用ContentTypestring响应头的Content-Type构造时必传且不可为空FileDownloadNamestring用于生成Content-Disposition: attachment响应头中的下载文件名LastModifiedDateTimeOffset?文件最后修改时间用于生成Last-Modified头与If-Modified-Since/If-Unmodified-Since条件判断EntityTagEntityTagHeaderValue?实体标签用于生成ETag头与If-Match/If-None-Match条件判断EnableRangeProcessingbool是否启用 HTTP Range 分段下载断点续传默认falseFileResult在 Mvc.Core 中有四个具体实现类结果类型数据源典型获取方式PhysicalFileResult磁盘绝对路径PhysicalFile(path, contentType, ...)VirtualFileResult虚拟路径 IFileProvidernew VirtualFileResult(...)或通过FileProvider定位FileStreamResult任意StreamFile(stream, contentType, ...)FileContentResult内存byte[]File(bytes, contentType, ...)值得注意的是FileContentResult与FileStreamResult的ExecuteResultAsync实现完全一致——通过RequestServices.GetRequiredServiceIActionResultExecutorT()从 DI 容器取出对应的执行器执行见 FileContentResult.cs 与 FileStreamResult.cs。这意味着你完全可以通过注册自定义的IActionResultExecutorFileContentResult来接管文件的发送过程——这正是“自定义 FileSender”这类扩展点的根基。2.1 从磁盘发送PhysicalFile 的完整用法DownloadFilesController.cs 是文件下载演示的核心它通过Controller.PhysicalFile展示了磁盘文件的多种发送姿势public IActionResult DownloadFromDisk() { var path Path.Combine(_hostingEnvironment.ContentRootPath, sample.txt); return PhysicalFile(path, text/plain, true); }PhysicalFile方法签名本质上是PhysicalFileResult的便捷工厂完整重载覆盖了“文件名 时间戳 ETag 是否允许 Range”的全部组合public IActionResult DownloadFromDisk_WithLastModifiedAndEtag() { var path Path.Combine(_hostingEnvironment.ContentRootPath, sample.txt); var lastModified new DateTimeOffset(year: 1999, month: 11, day: 04, hour: 3, minute: 0, second: 0, offset: new TimeSpan(0)); var entityTag new EntityTagHeaderValue(\Etag\); return PhysicalFile(path, text/plain, lastModified, entityTag, true); } public IActionResult DownloadFromDiskWithFileName() { var path Path.Combine(_hostingEnvironment.ContentRootPath, sample.txt); return PhysicalFile(path, text/plain, downloadName.txt); }要点说明第 3 个参数为enableRangeProcessing时返回的是PhysicalFile(path, contentType, enableRangeProcessing: true)即允许客户端发起Range请求传入fileDownloadName时响应会带上Content-Disposition: attachment; filenamedownloadName.txt; filename*UTF-8downloadName.txt头强制浏览器下载而非内联打开站点还演示了符号链接场景DownloadFromDiskSymlink通过File.CreateSymbolicLink创建指向sample.txt的软链接再发送。对应地PhysicalFileResultExecutor.GetFileInfo在检测到fileInfo.LinkTarget非空时会用ResolveLinkTarget(returnFinalTarget: true)解析到最终目标文件以获取正确的长度与修改时间见 PhysicalFileResultExecutor.cs。2.2 从内存与流发送File 方法与 FileContentResult当文件内容已经在内存或流中时使用Controller.File的其余重载// 字节数组 → FileContentResult public IActionResult DownloadFromBinaryData() { var data Encoding.UTF8.GetBytes(This is a sample text file from a binary array); return File(data, text/plain, true); } // 流 → FileStreamResult public IActionResult DownloadFromStreamWithFileName() { var stream new MemoryStream(); var writer new StreamWriter(stream); writer.Write(This is sample text from a stream); writer.Flush(); stream.Seek(0, SeekOrigin.Begin); return File(stream, text/plain, downloadName.txt); }File(byte[], contentType, enableRangeProcessing)返回的就是 readme 中点名的FileContentResultFile(stream, ...)返回FileStreamResult。内存场景同样支持 ETag 与 Range 组合例如public IActionResult DownloadFromStreamWithFileName_WithEtag() { var stream new MemoryStream(); var writer new StreamWriter(stream); writer.Write(This is sample text from a stream); writer.Flush(); stream.Seek(0, SeekOrigin.Begin); var entityTag new EntityTagHeaderValue(\Etag\); return File(stream, text/plain, downloadName.txt, lastModified: null, entityTag: entityTag, enableRangeProcessing: true); }2.3 从内嵌资源发送VirtualFileResult EmbeddedFileProviderEmbeddedFilesController.cs 演示了把程序集内嵌资源当作文件下发public IActionResult DownloadFileWithFileName() { return new VirtualFileResult(/Greetings.txt, text/plain) { FileProvider new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, FilesWebSite.EmbeddedResources), FileDownloadName downloadName.txt, EnableRangeProcessing true, }; }关键点VirtualFileResult的第一个参数是虚拟路径/Greetings.txt它不直接对应磁盘路径而是交给FileProvider解析EmbeddedFileProvider的第一个参数是程序集第二个参数FilesWebSite.EmbeddedResources是内嵌资源的根命名空间对应 EmbeddedResources/Greetings.txt 的编译结果若不显式指定FileProviderVirtualFileResultExecutor.GetFileProvider会回退到IWebHostEnvironment.WebRootFileProvider见 VirtualFileResultExecutor.cs即默认从wwwroot下解析文件。三、自定义 FileSender接管文件发送的两种路径readme 提到的“编写并注册自定义FileSender中间件”在 ASP.NET Core 中有两层含义对应两种扩展点从源码结构可以清晰看到路径一Http 层的IHttpResponseBodyFeature.SendFileAsync发送端 HookPhysicalFileResultExecutor.WriteFileAsyncInternal最终调用的是response.SendFileAsync(...)见 PhysicalFileResultExecutor.csif (range ! null) { return response.SendFileAsync(result.FileName, offset: range.From ?? 0L, count: rangeLength); } return response.SendFileAsync(result.FileName, offset: 0, count: null);这里的response.SendFileAsync是 IHttpResponseBodyFeature.SendFileAsync 的扩展调用其文档明确了语义path为磁盘绝对路径offset为起始偏移count为发送字节数null表示发送到文件末尾。VirtualFileResultExecutor则调用接受IFileInfo的SendFileAsync(fileInfo, ...)重载见 VirtualFileResultExecutor.cs。因此自定义中间件只要替换IHttpResponseBodyFeature就能拦截 Kestrel 默认的内核级零拷贝文件发送接入自己的发送逻辑例如云存储直传、限速、审计日志等。路径二Mvc 层的IActionResultExecutorT结果执行 Hook每个FileResult派生类的执行都通过 DI 解析对应的执行器。默认情况下FileContentResult与FileStreamResult使用FileResultExecutorBase的流拷贝路径64KB 缓冲 StreamCopyOperation见 FileResultHelper.WriteFileAsyncPhysicalFileResult与VirtualFileResult走SendFileAsync零拷贝路径。你可以在ConfigureServices中注册自定义执行器来替换默认行为例如services.AddSingletonIActionResultExecutorFileContentResult, MyFileSender();从代码结构看这是“自定义 FileSender”最贴合 MVC 语义的注册方式。四、HTTP 语义的底层实现缓存、条件请求与 Range 断点续传为什么FileResult要提供LastModified、EntityTag、EnableRangeProcessing这些属性答案全部在共享代码 FileResultHelper.cs 的SetHeadersAndLog方法中它是PhysicalFileResultExecutor与VirtualFileResultExecutor共用的头部处理核心见 FileResultExecutorBase.cs。整体流程如下Last-Modified 向下取整到秒HTTP 日期头精度为秒RoundDownToWholeSeconds保证与服务端比较时语义一致条件请求评估GetPreconditionState综合处理If-Match强比较、If-None-Match弱比较、If-Modified-Since、If-Unmodified-Since四个请求头得到Unspecified/NotModified/ShouldProcess/PreconditionFailed四种状态短路响应状态为NotModified时直接返回304为PreconditionFailed时返回412均不发送文件体设置内容头写入Content-Type、Content-Disposition当FileDownloadName非空时按 RFC 2183 生成attachment头并使用SetHttpFileName兼容 ASCII 与 UTF-8 两种文件名编码、Content-Length处理 Range仅当EnableRangeProcessing true且方法为 GET/HEAD 且If-Range校验通过时才进入SetRangeHeaders。SetRangeHeaders的行为可以归纳为一张可直接对照的状态表客户端请求服务端响应说明合法的单段Range: bytes0-6206 Partial ContentContent-Range 截断后的Content-Length返回指定字节段Range越界如bytes35-36或bytes-0416 Requested Range Not SatisfiableContent-Range: */lengthContent-Length: 0空响应体空 Range、bytes 、多段 Range如bytes1-4, 5-11忽略 Range返回完整文件200 OK解析失败或暂不支持多段EnableRangeProcessing false忽略 Range返回完整文件日志输出NotEnabledForRangeProcessingIf-Range校验失败忽略 Range返回完整文件保证客户端缓存与文件一致性这些行为并非文档臆测而是 FileResultTests.cs 中数百行功能测试的断言内容——例如FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequest用InlineData(0, 6, This is)等三组数据验证 206 响应体FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestNotSatisfiable验证 416 与空响应体FileFromDisk_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest_WithLastModifiedAndEtag验证If-Range命中时返回 206、IfRangeHeaderInvalid变体则验证未命中时退回完整 200 文件。这些测试都通过MvcTestFixtureFilesWebSite.Startup直接启动本 readme 对应的 FilesWebSite 站点来执行。五、文件上传IFormFile 模型绑定与 multipart 表单站点另一条主线是文件上传。UploadFilesController.cs 展示了IFormFile的两种绑定形态5.1 单文件上传IFormFile 属性 流读取Models/User.cs 中Biography属性直接声明为IFormFile控制器接收后通过OpenReadStream()读取内容[HttpPost(UploadFiles)] public async Taskobject Post(User user) { var resultUser new { Name user.Name, Age user.Age, Biography await user.ReadBiography() }; return resultUser; }User.ReadBiography展示了读取上传文件的标准姿势if (Biography ! null) { using (var reader new StreamReader(Biography.OpenReadStream())) { return await reader.ReadToEndAsync(); } }5.2 多文件字典上传IFormFile 集合 索引器绑定Models/Product.cs 演示了更复杂的形态——IDictionarystring, IEnumerableIFormFile一个键对应多个文件public class Product { public string Name { get; set; } public IDictionarystring, IEnumerableIFormFile Specs { get; set; } }控制器遍历字典取出每个文件的FileName并做ModelState校验[HttpPost(UploadProductSpecs)] public object ProductSpecs(Product product) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var files new Dictionarystring, Liststring(); foreach (var keyValuePair in product.Specs) { files.Add(keyValuePair.Key, keyValuePair.Value?.Select(formFile formFile?.FileName).ToList()); } return new { Name product.Name, Specs files }; }对应的 multipart 表单字段格式取自 FormFileUploadTest.cs 的UploadMultipleFiles测试Specs[0].Key传键名、Specs[0].Value传文件内容与文件名同一Value键可重复提交以形成多文件列表。测试断言了camera键绑定到camera_spec1.txt、camera_spec2.txt两个文件battery键绑定到两个 battery 文件完整验证了字典 集合的绑定能力。六、如何运行与验证FilesWebSite 作为功能测试站点最直接的验证方式是通过测试框架运行其配套测试也可以独立启动站点手动验证。方式一运行功能测试推荐覆盖最全dotnet test src/Mvc/test/Mvc.FunctionalTests/Mvc.FunctionalTests.csproj \ --filter FullyQualifiedName~FileResultTests|FullyQualifiedName~FormFileUploadTestFileResultTests.cs 通过http://localhost/DownloadFiles/DownloadFromDisk等地址验证磁盘文件、Range、ETag、Content-Disposition全链路FormFileUploadTest.cs 用MultipartFormDataContent构造 multipart 请求验证单文件与多文件绑定与读取。方式二手动启动站点站点的 launchSettings.json 预置了FilesWebSite配置Kestrel 监听https://localhost:5001;http://localhost:5000与 IIS Express 配置。直接运行后可用 curl 验证核心行为# 下载磁盘文件附带 Content-Disposition 下载名 curl -i http://localhost:5000/DownloadFiles/DownloadFromDiskWithFileName # Range 断点续传 curl -i -H Range: bytes0-6 http://localhost:5000/DownloadFiles/DownloadFromDisk # ETag If-None-Match 条件请求 curl -i -H If-None-Match: \Etag\ http://localhost:5000/DownloadFiles/DownloadFromDisk_WithLastModifiedAndEtag # 上传单文件 curl -F NameJohn -F Age23 -F Biographysample.txt http://localhost:5000/UploadFiles七、小结从测试站点到生产实践FilesWebSite 虽小却浓缩了 ASP.NET Core MVC 文件处理的全部核心链路下载侧File/PhysicalFile方法与FileContentResult、FileStreamResult、PhysicalFileResult、VirtualFileResult四种结果类型一一对应数据源覆盖字节数组、流、磁盘路径、内嵌资源HTTP 语义侧LastModified、EntityTag、EnableRangeProcessing三个属性驱动完整的缓存验证304/412、If-Range校验与 Range 断点续传206/416逻辑实现集中在 FileResultHelper.cs扩展侧通过替换IHttpResponseBodyFeature.SendFileAsync或注册自定义IActionResultExecutorT即可实现 readme 所言的“自定义 FileSender 中间件”接管内核级文件发送上传侧IFormFile支持单文件属性、流式读取与IDictionarystring, IEnumerableIFormFile字典多文件绑定三种形态。无论是实现带断点续传的大文件下载、给下载接口加 ETag 缓存还是设计复杂的多文件上传表单这个测试站点及其配套测试FileResultTests.cs、FormFileUploadTest.cs都是可以直接对照、复用的权威参考实现。【免费下载链接】aspnetcoreASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.项目地址: https://gitcode.com/GitHub_Trending/as/aspnetcore创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价