如何将ASP.NET文件上传API改为异步,支持校验期间用户自由操作应用?
实现方案说明
首先纠正一个认知误区:普通的异步接口(加async/await关键字)只是优化服务端的线程利用率,客户端仍然需要等待接口返回结果,无法实现你要的「上传后立刻跳转、后续主动收通知」的需求。你需要的是「请求响应+后台异步任务+主动推送」的架构,前后端改造如下:
一、后端(.NET Web API)改造
1. 基础数据结构调整
给你的文件表新增2个字段:
CheckStatus:int类型,0=校验中,1=校验成功,2=校验失败CheckRemark:nvarchar类型,存储校验失败的原因、病毒提示等信息
2. 上传接口改造
修改原接口逻辑,只做「接收文件、落盘、写初始数据库记录、提交校验任务到后台队列」,立刻返回响应,不再等待校验完成:
// 接口加异步逻辑仅优化服务端性能,核心是不等待校验完成就返回 public async Task<HttpResponseMessage> Create() { try { HttpRequest request = HttpContext.Current.Request; HttpPostedFile file = request.Files[0]; // 省略原有参数拼接逻辑 sqlDataAccess.ExecuteQuery("spCreateDokument", ar); int id = sqlDataAccess.GetParamValue<int>(ar, "@Id"); string filePath = Path.Combine(GetFolder(), id.ToString() + Path.GetExtension(file.FileName)); file.SaveAs(filePath); // 核心:把校验任务提交到后台队列,不阻塞当前请求 // 可以用Hangfire、ASP.NET Core HostedService+队列实现,这里以Hangfire为例 BackgroundJob.Enqueue(() => ExecuteFileCheckAsync(id, filePath)); // 返回202接受状态,同时返回文件id给前端后续关联通知 return Request.CreateResponse(HttpStatusCode.Accepted, new { FileId = id }); } catch (Exception ex) { throw ProcessException(ex, System.Reflection.MethodBase.GetCurrentMethod()); } } // 单独抽离的校验逻辑,后台独立运行 public async Task ExecuteFileCheckAsync(int fileId, string filePath) { try { // 调用第三方校验接口,这里是耗时1-2分钟的逻辑 var checkResult = await ThirdPartCheckService.DoCheckAsync(filePath); if(checkResult.IsSuccess) { // 更新数据库状态为校验成功 UpdateFileStatus(fileId, 1, "校验通过"); // 推送成功通知给前端 await NotificationHub.PushCheckResult(fileId, true, "文件校验通过,上传成功"); } else { // 更新数据库状态为校验失败,可同时删除无效文件 UpdateFileStatus(fileId, 2, checkResult.ErrorMsg); File.Delete(filePath); // 推送失败通知给前端 await NotificationHub.PushCheckResult(fileId, false, $"文件校验失败:{checkResult.ErrorMsg}"); } } catch(Exception ex) { // 异常处理,更新状态为失败,推送通知 UpdateFileStatus(fileId, 2, "校验服务异常"); await NotificationHub.PushCheckResult(fileId, false, "校验服务异常,请重新上传"); } }
3. 主动推送模块搭建
用ASP.NET SignalR实现WebSocket推送,只要用户页面没关,不管跳转到哪个路由都能收到通知:
// 定义通知Hub public class NotificationHub : Hub { // 推送校验结果给指定用户,这里可以根据你的用户体系关联ConnectionId public static async Task PushCheckResult(int fileId, bool isSuccess, string msg) { var context = GlobalHost.ConnectionManager.GetHubContext<NotificationHub>(); // 拿到当前上传用户的ConnectionId推送,也可以广播给指定用户组 await context.Clients.User(CurrentUserId).ReceiveCheckResult(new { FileId = fileId, IsSuccess = isSuccess, Message = msg }); } }
二、前端(Angular)改造
1. 全局SignalR连接服务
创建一个单例的通知服务,应用启动时就建立WebSocket连接,路由跳转不会销毁连接:
// notification.service.ts import { Injectable } from '@angular/core'; import * as signalR from '@microsoft/signalr'; import { MatSnackBar } from '@angular/material/snack-bar'; // 全局通知组件,可替换为你自己的实现 @Injectable({ providedIn: 'root' }) export class NotificationService { private hubConnection: signalR.HubConnection; constructor(private snackBar: MatSnackBar) { this.initConnection(); this.listenCheckResult(); } private initConnection() { this.hubConnection = new signalR.HubConnectionBuilder() .withUrl('你的API地址/notificationHub') .withAutomaticReconnect() // 自动重连 .build(); this.hubConnection.start().catch(err => console.error('连接通知服务失败:', err)); } private listenCheckResult() { this.hubConnection.on('ReceiveCheckResult', (result) => { // 全局弹出通知,不管用户在哪个页面都能看到 this.snackBar.open(result.message, '关闭', { duration: 5000, panelClass: result.isSuccess ? 'success-snack' : 'error-snack' }); // 可选:把结果存到状态管理里,方便后续文件列表页展示 }); } }
2. 上传逻辑调整
上传接口调用成功拿到fileId后,不需要等待后续结果,直接释放当前页面资源,用户可以正常跳转:
// 上传方法示例 uploadFile(file: File) { this.http.post('你的API地址/api/file/create', formData).subscribe(res => { this.snackBar.open('文件已提交,正在后台校验,请稍候等待结果通知', '关闭', { duration: 3000 }); // 这里可以直接做路由跳转,不需要等待校验完成 }); }
三、可选优化点
- 如果用户临时关闭页面再打开,可以加个接口拉取当前用户所有校验中的文件状态,页面加载时主动查询一次
- 后台校验任务添加重试机制,避免第三方服务临时不可用导致校验失败
- 大文件上传可以加分片逻辑,和当前校验逻辑不冲突
内容的提问来源于stack exchange,提问作者5ra
相关产品推荐
相关产品推荐

