ASP.NET Core MVC 如何禁用特定操作的会话计时器更新
问题根源
默认配置下,只要请求生命周期内访问过ISession(哪怕仅读取数据未做修改),会话中间件在请求管道收尾阶段都会自动触发分布式缓存的写入操作,刷新会话的滑动过期时间,这就是你观察到单次页面加载触发多次无意义SQL Server访问的原因。你需要的「正常读取会话数据、但跳过特定请求的过期时间刷新」需求,可通过以下两种可落地方案实现:
方案1:特性标记 + 管道拦截(实现成本最低)
该方案不需要替换默认缓存实现,仅通过请求管道加一层判断即可实现效果:
- 定义用于标记跳过刷新的特性:
using System.Reflection; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Controllers; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] public class SkipSessionSlidingRefreshAttribute : Attribute { } - 在请求配置中添加拦截逻辑,注意这段代码必须放在
UseSession()之后、路由映射之前:app.UseSession(); // 新增会话刷新拦截逻辑 app.Use(async (context, next) => { await next(); var currentEndpoint = context.GetEndpoint(); // 仅对带跳过标记的端点生效 if (currentEndpoint?.Metadata.GetMetadata<SkipSessionSlidingRefreshAttribute>() == null) return; var sessionFeature = context.Features.Get<ISessionFeature>(); if (sessionFeature?.Session is DistributedSession distributedSession) { // 标记会话已完成加载,跳过默认的过期刷新提交逻辑 // 该操作不影响请求过程中对会话数据的正常读取 distributedSession.GetType() .GetMethod("SetLoaded", BindingFlags.Instance | BindingFlags.NonPublic) ?.Invoke(distributedSession, new object[] { false }); } }); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); - 仅需要在不需要刷新会话计时器的Action/Controller上添加
[SkipSessionSlidingRefresh]标记即可。
注意:该方案用到了
DistributedSession的非公开方法,不同版本ASP.NET Core内部实现可能有变动,如果介意依赖非公开API,可选择方案2。
方案2:包装分布式缓存实现(兼容性最好)
该方案基于公开的IDistributedCache接口实现包装层,针对标记了跳过逻辑的请求,屏蔽仅用于刷新过期时间的缓存调用,兼容所有.NET Core 3.1及以上版本:
- 实现自定义条件缓存包装类:
using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.SqlServer; using Microsoft.Extensions.Options; public class ConditionalRefreshSqlServerCache : IDistributedCache { private readonly SqlServerCache _innerCache; private readonly IHttpContextAccessor _contextAccessor; public ConditionalRefreshSqlServerCache( IOptions<SqlServerCacheOptions> cacheOptions, IHttpContextAccessor contextAccessor) { _innerCache = new SqlServerCache(cacheOptions); _contextAccessor = contextAccessor; } // 所有读、删、改操作直接透传,不做修改 public byte[] Get(string key) => _innerCache.Get(key); public Task<byte[]> GetAsync(string key, CancellationToken token = default) => _innerCache.GetAsync(key, token); public void Remove(string key) => _innerCache.Remove(key); public Task RemoveAsync(string key, CancellationToken token = default) => _innerCache.RemoveAsync(key, token); public void Set(string key, byte[] value, DistributedCacheEntryOptions options) => _innerCache.Set(key, value, options); public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) => _innerCache.SetAsync(key, value, options, token); // 拦截仅用于刷新过期的调用 public void Refresh(string key) { if (!ShouldSkipRefresh()) _innerCache.Refresh(key); } public Task RefreshAsync(string key, CancellationToken token = default) { return ShouldSkipRefresh() ? Task.CompletedTask : _innerCache.RefreshAsync(key, token); } private bool ShouldSkipRefresh() { var httpContext = _contextAccessor.HttpContext; if (httpContext == null) return false; var endpoint = httpContext.GetEndpoint(); return endpoint?.Metadata.GetMetadata<SkipSessionSlidingRefreshAttribute>() != null; } } - 注册服务时替换默认的分布式缓存实现:
// 先注册HttpContextAccessor用于获取当前请求上下文 builder.Services.AddHttpContextAccessor(); // 保留原有SqlServer缓存配置 builder.Services.AddDistributedSqlServerCache(options => { options.ConnectionString = builder.Configuration.GetConnectionString("SessionDb"); options.SchemaName = "dbo"; options.TableName = "AppSession"; }); // 替换为自定义的条件刷新缓存实现 builder.Services.AddSingleton<IDistributedCache, ConditionalRefreshSqlServerCache>();
使用注意事项
- 仅给页面加载时自动触发的、不代表用户主动活跃的附属接口(比如控件绑定的拉取数据Web服务)添加跳过标记,不要给用户主动触发的核心操作接口加标记,避免会话提前过期。
- 如果在标记了跳过刷新的接口中需要修改会话数据,记得手动调用
await HttpContext.Session.CommitAsync()保证数据持久化。 - 两种方案都不需要修改原有读取会话的业务代码,对现有逻辑侵入性极低。
内容的提问来源于stack exchange,提问作者ShawnO
相关产品推荐
相关产品推荐

