ASP.NET Core中如何确保仅返回异步请求的最新结果?
问题描述
- 使用Syncfusion AutoComplete控件时,每次输入按键都会触发对
HomeController.GetGames方法的请求,该方法会调用外部GiantBomb API获取游戏数据 - 连续输入(如输入"Call of Duty")时,早期请求(如输入"C"的请求)耗时更长,最后返回会覆盖最新请求的结果,导致显示错误数据集
- 尝试通过Session持久化
CancellationTokenSource集合来取消旧请求,但未生效,核心原因是对CancellationTokenSource的序列化存储存在误解
错误原因分析
CancellationTokenSource无法序列化:CancellationTokenSource是包含实时状态和回调的复杂对象,无法通过JSON序列化后存储到Session中。反序列化得到的是全新对象,和原对象无关联,调用Cancel()不会对之前的请求产生任何影响- 缺少前端请求限流:未对前端请求做防抖处理,导致不必要的频繁请求,加剧了旧请求覆盖新结果的问题
解决方案
一、前端实现防抖逻辑
修改GiantBombAutocomplete_Filtering方法,添加防抖延迟,用户停止输入300ms后再发送请求,减少无效请求数量:
let debounceTimer; function GiantBombAutocomplete_Filtering(args) { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { args.preventDefault(); // 阻止默认立即请求 const autocompleteObj = document.getElementById('GiantBombAutocomplete').ej2_instances[0]; autocompleteObj.dataSource.executeQuery(args.query) .then(e => { autocompleteObj.dataSource = e.result; autocompleteObj.openPopup(); }) .catch(err => console.error(err)); }, 300); // 可根据需求调整延迟时间 }
二、后端正确实现请求取消(不依赖Session)
改用IMemoryCache存储用户当前活跃的CancellationTokenSource,直接保留对象引用,实现旧请求的有效取消:
1. 控制器注入IMemoryCache
public class HomeController : Controller { private readonly ILogger<HomeController> _logger; private readonly IMemoryCache _memoryCache; private const string _ctsCacheKeyPrefix = "ActiveCts_"; public HomeController(ILogger<HomeController> logger, IMemoryCache memoryCache) { _logger = logger; _memoryCache = memoryCache; } // 保留原有其他方法...
2. 重构GetGames方法
public async Task<IActionResult> GetGames([FromBody]Data dataManager, CancellationToken requestCancellationToken) { var userId = HttpContext.Session.Id; var cacheKey = _ctsCacheKeyPrefix + userId; // 取消并清理之前的请求令牌 if (_memoryCache.TryGetValue(cacheKey, out CancellationTokenSource previousCts)) { previousCts.Cancel(); previousCts.Dispose(); } // 创建新的关联令牌源 var currentCts = CancellationTokenSource.CreateLinkedTokenSource(requestCancellationToken); _memoryCache.Set(cacheKey, currentCts, new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(5) // 设置过期时间防止内存泄漏 }); try { var games = await GiantBombConnection.GetGames( dataManager.where[0].value, currentCts.Token); return Json(games); } catch (OperationCanceledException) { // 请求被取消时返回空结果 return Json(new List<Game>()); } finally { _memoryCache.Remove(cacheKey); currentCts.Dispose(); } }
3. 简化API调用类的取消逻辑
public class GiantBombConnection { private const string BaseUrl = "https://www.giantbomb.com/api"; private const string ApiKey = "api key"; public static async Task<List<Game>> GetGames(string name, CancellationToken cancellationToken) { var url = $"{BaseUrl}/games"; var query = HttpUtility.ParseQueryString(string.Empty); query["api_key"] = ApiKey; query["format"] = "json"; query["filter"] = $"name:{name}"; using (var client = new HttpClient()) { var requestMessage = new HttpRequestMessage(HttpMethod.Get, $"{url}?{query}"); requestMessage.Headers.Add("User-Agent", "MvcMovie - App"); // 发送请求时传入取消令牌,支持请求过程中取消 var response = await client.SendAsync(requestMessage, cancellationToken); response.EnsureSuccessStatusCode(); var apiResponse = await response.Content.ReadAsStringAsync(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); // 检查是否已取消 var gamesResponse = JsonConvert.DeserializeObject<GamesResponse>(apiResponse); return gamesResponse?.Results ?? new List<Game>(); } } }
三、额外优化建议
- 请求标识校验:前端给每个请求添加唯一ID,后端返回结果时携带该ID,前端仅处理最新ID对应的结果,即使旧请求未被取消也不会覆盖新结果
- API结果缓存:对相同搜索关键词的API响应进行缓存,避免重复调用外部API,提升响应速度
内容的提问来源于stack exchange,提问作者TomR
相关产品推荐
相关产品推荐

