使用Ajax与IProgress获取进度时状态值始终为0的问题排查
问题分析与解决方案
核心问题:控制器实例的请求作用域特性
ASP.NET Core中,控制器是请求作用域的——每一个HTTP请求都会创建一个全新的控制器实例。在GeneratePrintRecipeCards(POST请求)中更新的_generateRecipeCardsStatus,属于当前POST请求对应的控制器实例;而GeneratePrintRecipeCardsStatus(GET请求)每次被调用时,都会创建新的控制器实例,这个新实例的_generateRecipeCardsStatus是初始值0,自然读不到之前更新的进度。
此外还有两个次要问题:
DoAsync方法标记了async但内部无真正异步操作(ToList()、foreach均为同步执行),会阻塞POST请求线程,导致轮询的GET请求在任务结束前可能无法获取有效进度。- 服务中
donePointer被重复自增(++donePointer和donePointer++),会导致进度计算错误。
解决方案
1. 使用全局存储共享进度值
放弃控制器私有字段,改用IMemoryCache全局存储进度值,确保不同请求能访问到同一进度数据:
public class MenuPrintoutController : BaseController { private readonly IMenuLabelService _menuLabelService; private readonly IMemoryCache _cache; // 用用户标识+任务ID生成唯一缓存键,避免不同用户/任务进度冲突 private string GetProgressCacheKey(int menuHeaderId) => $"RecipeCardsProgress_{User.Identity.Name}_{menuHeaderId}"; public MenuPrintoutController(IMenuLabelService menuLabelService, IMemoryCache cache) { _menuLabelService = menuLabelService; _cache = cache; } public int GeneratePrintRecipeCardsStatus(int menuHeaderId) { return _cache.TryGetValue(GetProgressCacheKey(menuHeaderId), out int progress) ? progress : 0; } private void ReportProgress(int progress, int menuHeaderId) { // 设置10分钟过期时间,避免内存泄漏 _cache.Set(GetProgressCacheKey(menuHeaderId), progress, TimeSpan.FromMinutes(10)); } [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> GeneratePrintRecipeCards(int id, int menuHeaderId) { try { // 初始化进度为0 _cache.Set(GetProgressCacheKey(menuHeaderId), 0, TimeSpan.FromMinutes(10)); var progress = new Progress<int>(p => ReportProgress(p, menuHeaderId)); var data = await _menuLabelService.DoAsync(menuHeaderId, progress); TempData.Put("PrintRecipeCards", data); // 任务完成后清除缓存 _cache.Remove(GetProgressCacheKey(menuHeaderId)); return Json(new { Success = true }); } catch (Exception exp) { _cache.Remove(GetProgressCacheKey(menuHeaderId)); return Json(new { ErrorMsg = exp.Message }); } } }
2. 修正服务中的异步逻辑与进度计算
将同步数据库操作改为异步,避免阻塞请求线程,同时修复donePointer重复自增问题:
public async Task<object> DoAsync(int menuHeaderId, IProgress<int> progress) { // 改用ToListAsync异步查询数据库 var menuFoodItemsList = await _context.FoodItemUnits .Where(x => x.MenuSectionFoodItemUnits.Any(y => y.Menusection.MenuheaderId == menuHeaderId)) .Select(x => x.Fooditem) .ToListAsync(); var menuFoodItems = new List<PrintLabel>(); var donePointer = 0; foreach (var menuFoodItem in menuFoodItemsList) { menuFoodItems.Add(menuFoodItem); // ... 执行业务逻辑 donePointer++; // 计算进度,强制转为浮点除法避免整数截断 var currentProgress = (int)((double)donePointer / menuFoodItemsList.Count * 100); progress.Report(currentProgress); // 可选:添加短暂延迟,避免进度更新过于频繁 await Task.Delay(10); } return menuFoodItems; }
3. (进阶)用SignalR替代轮询
若场景允许,推荐使用SignalR实现实时进度推送,比定时轮询更高效,也无需维护缓存:
- 注册SignalR服务并创建进度Hub
- 前端连接Hub,实时接收进度推送
- 后台任务通过Hub向前端主动推送进度
内容的提问来源于stack exchange,提问作者Karim Ali
相关产品推荐
相关产品推荐

