.NET Core API基于MediatR模式下载wwwroot文件实现方案
实现问题说明
当前代码运行报错、功能不生效的核心原因如下:
- 返回类型定义完全不符合文件下载要求:MediatR请求、处理器、控制器接口全部定义为返回
bool类型,无法向客户端返回文件流数据 - 文件读取逻辑错误:本地wwwroot目录下的文件无需通过
WebClient发起HTTP请求读取,且代码存在路径重复拼接问题,会触发文件找不到异常 - 逻辑判断无效:数据库查询使用
Where()后直接判断非空,Where()返回的IQueryable对象永远不为null,判断逻辑完全不生效 - 存在安全风险:未做文件名合法性校验,存在路径遍历攻击漏洞,未校验文件真实存在性
- 依赖版本过时:使用已废弃的
IHostingEnvironment接口,.NET Core 3.0+版本需替换为IWebHostEnvironment
MediatR模式下正确实现方案
保持MediatR业务层与Web层解耦,业务逻辑层返回文件数据传输对象,控制器层负责转换为HTTP文件响应。
1. 定义文件传输模型与Command
在Application层新增文件下载返回Dto,调整Command的返回值类型:
// 文件下载传输模型 public class FileDownloadResult { public bool IsSuccess { get; set; } public string ErrorMessage { get; set; } public byte[] FileContent { get; set; } public string FileName { get; set; } public string ContentType { get; set; } } // 修改Command的IRequest泛型参数为上述模型 public class DownloadMPWorthyFileCommand : IRequest<FileDownloadResult> { public string FileName { get; set; } public string PageName { get; set; } public string IdeaNumber { get; set; } }
2. 重写Command Handler处理逻辑
移除无用的冗余依赖注入,补充校验逻辑,直接读取本地文件流:
using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; public class DownloadMPWorthyFileCommandHandler : IRequestHandler<DownloadMPWorthyFileCommand, FileDownloadResult> { private readonly IKaizenDBContext _context; private readonly IWebHostEnvironment _webHostEnv; // 仅注入实际用到的依赖,删除IMapper、IEmailService等无用注入 public DownloadMPWorthyFileCommandHandler(IKaizenDBContext context, IWebHostEnvironment webHostEnv) { _context = context; _webHostEnv = webHostEnv; } public async Task<FileDownloadResult> Handle(DownloadMPWorthyFileCommand request, CancellationToken cancellationToken) { // 文件名合法性校验,拦截路径遍历攻击 if (string.IsNullOrWhiteSpace(request.FileName) || request.FileName.Contains("..") || Path.IsPathRooted(request.FileName)) { return new FileDownloadResult { IsSuccess = false, ErrorMessage = "非法文件名" }; } // 查询数据库文件记录,可在此处扩展用户权限校验 var fileRecord = await _context.DocumentUploaded .FirstOrDefaultAsync(e => e.IdeaNumber == request.IdeaNumber && e.PageName == request.PageName && e.FileName == request.FileName, cancellationToken); if (fileRecord == null) { return new FileDownloadResult { IsSuccess = false, ErrorMessage = "文件记录不存在" }; } // 拼接wwwroot下文件的真实物理路径 var filePhysicalPath = Path.Combine(_webHostEnv.WebRootPath, "document", request.FileName); if (!File.Exists(filePhysicalPath)) { return new FileDownloadResult { IsSuccess = false, ErrorMessage = "服务器文件丢失" }; } // 读取文件内容,识别文件MIME类型 var fileBytes = await File.ReadAllBytesAsync(filePhysicalPath, cancellationToken); var fileExt = Path.GetExtension(request.FileName).ToLowerInvariant(); var contentType = fileExt switch { ".pdf" => "application/pdf", ".doc" => "application/msword", ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".xls" => "application/vnd.ms-excel", ".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".png" => "image/png", ".jpg" or ".jpeg" => "image/jpeg", ".txt" => "text/plain", _ => "application/octet-stream" }; return new FileDownloadResult { IsSuccess = true, FileContent = fileBytes, FileName = request.FileName, ContentType = contentType }; } }
3. 修改控制器接口
调整接口返回类型为IActionResult,根据Handler返回结果生成响应:
[HttpPost("DownloadMPWorthyFile")] public async Task<IActionResult> DownloadMPWorthyFile([FromBody] DownloadMPWorthyFileCommand command) { var downloadResult = await Mediator.Send(command); if (!downloadResult.IsSuccess) { return BadRequest(new { message = downloadResult.ErrorMessage }); } // 返回文件流,触发浏览器下载行为 return File(downloadResult.FileContent, downloadResult.ContentType, downloadResult.FileName); }
前端对接注意事项
- 若使用Angular HttpClient发起Post请求,必须设置
responseType: 'blob',拿到二进制Blob对象后通过URL.createObjectURL生成临时链接,触发浏览器下载 - 如果希望简化前端调用,可将接口改为
[HttpGet],参数通过QueryString传递,前端可直接通过a标签、window.open触发下载,无需额外处理Blob流
内容的提问来源于stack exchange,提问作者sonali borekarr
相关产品推荐
相关产品推荐

