EF Core查询已入库数据需退出应用才刷新问题排查
问题现象
尽管保存操作确实已成功更新数据库中的记录,但客户端调用接口获取玩家列表时始终返回旧数据,必须等用户退出应用重启后,无需额外操作就能正常显示最新的玩家数据。
相关实现代码
- 客户端调用API获取所有玩家的方法
public async Task<List<Players>> GetALLPlayers() { List<Players> _result = new List<Players>(); EnsureHttpClientCreated(); var uri = new Uri(string.Format(Constants.BaseUrl + Constants.ApiSegmant + Constants.GetAllPlayers, string.Empty)); await AddAuthenicationHeader(); var response = await httpClient.GetAsync(uri); if (response.IsSuccessStatusCode) { var byteArray = await response.Content.ReadAsByteArrayAsync(); var content = Encoding.UTF8.GetString(byteArray, 0, byteArray.Length); _result = JsonConvert.DeserializeObject<List<Players>>(content); } return _result.ToList(); }
- HttpClient初始化创建逻辑
private HttpClient httpClient; private HttpClientHandler _httpClientHandler; public HttpApi() { _timeout = Timeout ?? TimeSpan.FromSeconds(90); CreateHttpClient(); db = new ThehockeylABSqlLiteDB(); } private void CreateHttpClient() { _httpClientHandler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip }; httpClient = new HttpClient(_httpClientHandler, false) { Timeout = _timeout }; httpClient.DefaultRequestHeaders.UserAgent .ParseAdd(ClientUserAgent); if (!string.IsNullOrWhiteSpace(Constants.BaseUrl)) { httpClient.BaseAddress = new Uri(Constants.BaseUrl); } httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson)); } private void EnsureHttpClientCreated() { if (httpClient == null) { CreateHttpClient(); } }
- 服务端PlayerController的查询接口
private readonly IPlayerInterface db; public PlayerController(IPlayerInterface classes) { db = classes; } [HttpGet("GetALLPlayers")] [Authorize] public List<Players> GetALlPlayers() { try { var test = db.GetAllPlayers(); } catch(Exception ex) { } return db.GetAllPlayers(); }
- 服务层PlayerService查询实现
public class PlayerService : IPlayerInterface { AppDBContext db; public PlayerService(AppDBContext dBContext) { db = dBContext; } public List<Players> GetAllPlayers() { return db.Players.Where(w => w.IsActive == true && w.IsDeleted == false).Include(i => i.Notes).ToList(); } // 服务类其余实现省略 }
- DI服务注册配置
services.AddScoped<IPlayerInterface, PlayerService>();
- 客户端DataGrid数据绑定逻辑
public async Task<int> BindGrid() { var players =await api.GetALLPlayers(); dgPlayers.ItemsSource = null; dgPlayers.ItemsSource = players; //此处获取的玩家列表仍为旧数据,计数与库中实际数据不符 playersCount.Text = $"Total Players {players.Count().ToString()}"; return 1; }
问题解答
判断结论
你提出的「添加AsNoTracking绕过EF跟踪缓存」的判断是错误的,该方案无法解决当前问题。
根本原因
问题出在客户端HttpClient默认开启的GET响应缓存,和EF Core跟踪缓存无关:
- EF Core的跟踪缓存是单个DbContext实例级别的,你当前IPlayerInterface注册为Scoped生命周期,正常情况下默认注册的AppDBContext也是Scoped生命周期,每个HTTP请求都会生成全新的DbContext实例,请求结束后实例会被释放,跨请求不会残留跟踪缓存,因此AsNoTracking不解决这个问题。
- 你初始化HttpClientHandler时没有配置缓存策略,.NET的HttpClientHandler默认会对相同URI的GET请求做本地内存缓存:如果服务端响应没有返回明确的禁止缓存头,后续同地址请求会直接返回本地缓存的旧响应,根本不会发起真实的HTTP请求到服务端。当你重启应用后,HttpClient实例被销毁,内存缓存清空,第一次请求自然能拿到最新数据,和你描述的现象完全匹配。
额外提两个代码里的非核心问题:
- 服务端Controller方法里冗余调用了两次
GetAllPlayers(),还吞掉了所有异常,不利于排查问题 - 可以顺手检查下AppDBContext的DI注册,确保没有被错误注册为Singleton,单例DbContext才会出现全局跟踪缓存导致数据不更新的问题
正确解决方案
客户端侧(快速修复)
修改HttpClient初始化逻辑,显式关闭自动缓存:
_httpClientHandler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, // 新增:彻底关闭HTTP响应缓存 CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore) };
也可以直接给HttpClient全局配置默认请求头,强制不使用缓存:
httpClient.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue { NoCache = true, NoStore = true };
服务端侧(兜底兼容)
给查询接口增加响应缓存特性,明确告知所有客户端、代理节点不要缓存该接口响应,避免其他类型客户端踩同样的坑:
[HttpGet("GetALLPlayers")] [Authorize] // 新增:禁用接口响应缓存 [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] public List<Players> GetALlPlayers() { // 建议删掉冗余的try块重复查询逻辑,异常处理加日志不要直接吞 return db.GetAllPlayers(); }
额外优化
针对只读的列表查询场景,你依然可以在EF Core查询里加上AsNoTracking(),这能减少实体跟踪开销、提升查询性能,但这属于性能优化项,不是解决当前旧数据问题的必须手段。
内容的提问来源于stack exchange,提问作者dotnetdevcsharp
相关产品推荐
相关产品推荐

