ASP.NET Web API文件验证属性重复读取请求内容报错解决方案咨询
解决ActionFilter中读取文件后Action无法重复读取流的问题
这个问题我之前也碰到过!核心原因就是HttpRequest的Content流是单向只读的,一旦在过滤器里读完验证,Action里再尝试读取就会因为流已经到末尾而报错。下面是完整的解决方案,同时优化了原代码里的异步处理和细节问题:
核心思路
在过滤器中一次性完成文件读取、验证,然后把验证通过的文件数据(文件名、字节数组)存入ActionContext.Items字典中,供后续的Action方法直接使用,完全避免二次读取Request流。
修改后的ValidateFileAttribute代码
public class ValidateFileAttribute : ActionFilterAttribute { // 定义允许的文件扩展名和对应魔数 private readonly List<string> _validExtensions = new() { "pdf", "doc", "docx" }; private readonly List<string> _validMagicNumbers = new() { "25-50-44-46", "D0-CF-11-E0", "50-4B-03-04" }; private const int MaxFileSize = 3 * 1024 * 1024; // 3MB public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken) { // 先判断是否为多部分表单请求 if (!actionContext.Request.Content.IsMimeMultipartContent()) { actionContext.Response = actionContext.Request.CreateErrorResponse( HttpStatusCode.BadRequest, "请求格式错误,请使用multipart/form-data格式上传文件"); return; } try { // 读取所有文件内容 var provider = await actionContext.Request.Content.ReadAsMultipartAsync(cancellationToken); var validatedFiles = new List<(string FileName, byte[] FileBytes)>(); foreach (var content in provider.Contents) { // 获取文件名(处理可能带的双引号) var fileName = content.Headers.ContentDisposition?.FileName?.Trim('"') ?? string.Empty; if (string.IsNullOrEmpty(fileName)) { actionContext.Response = actionContext.Request.CreateErrorResponse( HttpStatusCode.BadRequest, "存在未命名的上传文件"); return; } // 读取文件字节数组 var fileBytes = await content.ReadAsByteArrayAsync(cancellationToken); if (fileBytes == null || fileBytes.Length == 0) { actionContext.Response = actionContext.Request.CreateErrorResponse( HttpStatusCode.BadRequest, $"文件{fileName}为空,请重新上传"); return; } // 1. 验证扩展名 var extension = Path.GetExtension(fileName).TrimStart('.').ToLowerInvariant(); if (!_validExtensions.Contains(extension)) { actionContext.Response = actionContext.Request.CreateErrorResponse( HttpStatusCode.BadRequest, $"文件{fileName}格式不支持,仅允许:{string.Join(", ", _validExtensions)}"); return; } // 2. 验证魔数(取前4个字节) var magicNumber = BitConverter.ToString(fileBytes, 0, 4); if (!_validMagicNumbers.Contains(magicNumber, StringComparer.OrdinalIgnoreCase)) { actionContext.Response = actionContext.Request.CreateErrorResponse( HttpStatusCode.BadRequest, $"文件{fileName}格式与扩展名不符,请检查文件是否损坏"); return; } // 3. 验证文件大小 if (fileBytes.Length > MaxFileSize) { actionContext.Response = actionContext.Request.CreateErrorResponse( HttpStatusCode.BadRequest, $"文件{fileName}大小超过3MB限制"); return; } // 验证通过,加入列表 validatedFiles.Add((fileName, fileBytes)); } // 将验证后的文件存入ActionContext,供Action使用 actionContext.ActionContext.Items["ValidatedFiles"] = validatedFiles; } catch (Exception ex) { actionContext.Response = actionContext.Request.CreateErrorResponse( HttpStatusCode.InternalServerError, $"文件上传处理失败:{ex.Message}"); } } }
修改后的Upload Action代码
[ValidateFile] [Route("upload")] [HttpPost] public async Task<DocumentUploadResponse> Upload() { // 从ActionContext中获取验证后的文件数据 if (!ActionContext.Items.TryGetValue("ValidatedFiles", out var filesObj) || filesObj is not List<(string FileName, byte[] FileBytes)> validatedFiles) { return new DocumentUploadResponse { Success = false, Message = "未获取到有效的上传文件" }; } var response = new DocumentUploadResponse { Success = true, UploadedFileNames = new List<string>() }; foreach (var (fileName, fileBytes) in validatedFiles) { // 转换为Base64字符串 var base64File = Convert.ToBase64String(fileBytes); // 执行数据库存储操作(示例) // var document = new Document // { // FileName = fileName, // FileContentBase64 = base64File, // UploadTime = DateTime.UtcNow // }; // _dbContext.Documents.Add(document); // await _dbContext.SaveChangesAsync(); response.UploadedFileNames.Add(fileName); } return response; } // 假设的响应模型 public class DocumentUploadResponse { public bool Success { get; set; } public string? Message { get; set; } public List<string>? UploadedFileNames { get; set; } }
关键优化点说明
- 避免重复读流:过滤器中一次性读取所有文件并验证,将结果存入
ActionContext.Items,Action直接取用,彻底解决流只能读一次的问题。 - 异步处理优化:去掉了原代码中
Task.Factory.StartNew和.Result的错误用法,改用await避免异步死锁和线程阻塞。 - 细节处理:
- 处理文件名可能带的双引号(ContentDisposition.FileName默认会用双引号包裹)。
- 用
Path.GetExtension更可靠地获取文件扩展名。 - 魔数验证直接取前4个字节生成字符串,比Substring更准确。
- 错误提前终止:一旦某一步验证失败,立即返回错误响应,不再继续处理其他文件。
内容的提问来源于stack exchange,提问作者rajk
相关产品推荐
相关产品推荐

