ASP.NET Core OData自定义缓存特性触发DbContext已释放异常
ASP.NET Core OData 接口自定义缓存特性抛出DbContext已释放异常
场景说明
- 正在为ASP.NET Core应用开发OData API,需要为接口实现缓存能力
- 现有架构限制:所有端点均返回查询服务层直接提供的未执行
IQueryable类型结果,查询逻辑未在服务层执行,因此无法直接在服务层级实现缓存逻辑
控制器实现代码如下:
public class TagsController : ODataController { private readonly ITagService _tagService; private readonly ILogger<TagsController> _logger; public TagsController(ITagService tagService, ILogger<TagsController> logger) { _tagService = tagService; _logger = logger; } [HttpGet("odata/tags")] [Tags("Odata")] [AllowAnonymous] [EnableCachedQuery] public ActionResult<IQueryable<Tag>> Get() { try { return Ok(_tagService.GetAll()); } catch (Exception ex) { _logger.LogError(ex, "Some unknown error has occurred."); return BadRequest(); } } }
初始实现方案
最初尝试通过扩展OData原生EnableQuery特性实现缓存逻辑,自定义EnableCachedQuery特性:
- 重写
OnActionExecuting方法校验缓存命中状态 - 重写
OnActionExecuted方法将结果写入缓存 - 缓存规则:以请求路径+查询字符串作为缓存键,使用
IMemoryCache设置15分钟绝对过期时间
特性初始实现代码如下:
public class EnableCachedQuery : EnableQueryAttribute { private IMemoryCache _memoryCache; public EnableCachedQuery() { _memoryCache = new MemoryCache(new MemoryCacheOptions()); } public override void OnActionExecuting(ActionExecutingContext actionContext) { //var url = GetAbsoluteUri(actionContext.HttpContext); var path = actionContext.HttpContext.Request.Path + actionContext.HttpContext.Request.QueryString; //check cache if (_memoryCache.TryGetValue(path, out ObjectResult value)) { actionContext.Result = value; } else { base.OnActionExecuting(actionContext); } } public override void OnActionExecuted(ActionExecutedContext context) { if (context.Exception != null) return; var path = context.HttpContext.Request.Path + context.HttpContext.Request.QueryString; var cacheEntryOpts = new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(15)); base.OnActionExecuted(context); _memoryCache.Set(path, context.Result, cacheEntryOpts); } }
异常现象
- 首次请求可正常返回应用了OData筛选、查询条件的正确数据,写入缓存时
context.Result存储的是ObjectResult类型对象 - 第二次命中缓存的请求执行时抛出异常,说明缓存存储的并非可直接返回给客户端的最终输出结果
错误信息如下:
Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances. Object name: 'ApplicationDbContext'.
补充:上下文相关代码
项目使用的ApplicationDbContext继承自IdentityDbContext,为所有继承AuditableEntity的实体配置了软删除全局查询过滤器,同时加载了程序集中的实体配置与种子数据,实现代码如下:
public class ApplicationDbContext : IdentityDbContext<User, Account, Session>, IApplicationDbContext { public ApplicationDbContext( DbContextOptions options, IApplicationUserService currentUserService, IDomainEventService domainEventService, IBackgroundJobService backgroundJob, IDomainEventService eventService, IDateTime dateTime) : base(options, currentUserService, domainEventService, backgroundJob, dateTime) { } public DbSet<Tag> Tags => Set<Tag>(); protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); var entityTypes = builder.Model.GetEntityTypes() .Where(c => typeof(AuditableEntity).IsAssignableFrom(c.ClrType)) .ToList(); foreach (var type in entityTypes) { var parameter = Expression.Parameter(type.ClrType); var deletedCheck = Expression.Lambda (Expression.Equal(Expression.Property(parameter, nameof(AuditableEntity.Deleted)), Expression.Constant(false)), parameter); type.SetQueryFilter(deletedCheck); } builder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly); builder.ApplySeedsFromAssembly(typeof(ApplicationDbContext).Assembly); } }
问题根因
- 缓存内容错误:调用
base.OnActionExecuted(context)后,context.Result里包裹的仍然是未执行的IQueryable对象,这个对象持有对当前请求作用域ApplicationDbContext的引用。请求结束后DI容器会释放该DbContext实例,第二次请求返回缓存的ObjectResult时,框架执行IQueryable查询会直接访问已释放的DbContext,抛出异常。 - 内存缓存实例生命周期错误:在特性构造函数中直接new
MemoryCache实例,而MVC中过滤器特性默认是单例复用的,该缓存实例不受DI容器管理,容易引发内存泄漏、缓存不一致问题。 - 结果对象复用风险:直接缓存
ObjectResult实例,不同请求复用同一个结果对象会引发并发状态冲突。
修复方案
- 不要在特性中自行初始化
IMemoryCache,通过当前请求的RequestServices从DI容器获取缓存实例,保持和应用全局缓存生命周期一致。 - 缓存写入前必须将
IQueryable执行成内存中的强类型集合(比如List<T>),切断和DbContext的引用关系,只缓存最终可直接返回的静态数据。 - 缓存命中时,基于缓存的内存集合新建
OkObjectResult返回,不要复用历史请求的结果对象。
修复后的EnableCachedQuery实现参考:
public class EnableCachedQuery : EnableQueryAttribute { public override void OnActionExecuting(ActionExecutingContext actionContext) { var path = actionContext.HttpContext.Request.Path + actionContext.HttpContext.Request.QueryString; var memoryCache = actionContext.HttpContext.RequestServices.GetRequiredService<IMemoryCache>(); // 命中缓存直接返回内存集合包装的结果 if (memoryCache.TryGetValue(path, out object cachedData)) { actionContext.Result = new OkObjectResult(cachedData); return; } base.OnActionExecuting(actionContext); } public override void OnActionExecuted(ActionExecutedContext context) { if (context.Exception != null || context.Result is not ObjectResult objResult) { base.OnActionExecuted(context); return; } // 先执行基类逻辑,完成OData查询条件应用 base.OnActionExecuted(context); // 重新获取执行OData逻辑后的结果 objResult = context.Result as ObjectResult; if (objResult?.Value is IQueryable queryable) { // 将IQueryable执行成内存集合,切断和DbContext的引用 var cachedData = queryable.Cast<object>().ToList(); // 替换当前响应结果为内存集合,避免后续执行再次访问DbContext context.Result = new OkObjectResult(cachedData); var memoryCache = context.HttpContext.RequestServices.GetRequiredService<IMemoryCache>(); var cacheEntryOpts = new MemoryCacheEntryOptions() .SetAbsoluteExpiration(TimeSpan.FromMinutes(15)); // 只缓存内存集合,不缓存结果对象 memoryCache.Set(path, cachedData, cacheEntryOpts); } } }
内容的提问来源于stack exchange,提问作者Mohamed Salah
相关产品推荐
相关产品推荐

