You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET Core 3.1 Scoped上下文在路由执行完成前被释放问题

Scoped生命周期上下文在路由执行完成前被提前释放问题

环境信息

  • 运行环境:Windows 10 + .NET Core 3.1 MVC
  • 异常表现:注册为Scoped生命周期的数据库上下文、工作单元实例,在控制器逻辑及响应完成回调未执行完毕时就被调用Dispose,调整服务生命周期无法解决。

复现代码

Unit Of Work 实现

public interface IUnitOfWork
{
    void Dispose();
    bool GetContextDisposed();
}

public class UnitOfWork : IDisposable, IUnitOfWork
{
    private ISomeContext _context;
    public ISomeRepository someRepository;
    public UnitOfWork(ISomeContext context)
    {
        _context = context;
        someRepository = new SomeRepository(_context);
    }

    public void Dispose()
    {
        _context.Dispose();
    }

    public bool GetContextDisposed()
    {
        return _context.GetDisposed(); 
    }
}

上下文定义与实现

public interface ISomeContext
{
    void Dispose();
    public bool GetDisposed();
}

public class SomeContext : IDisposable, ISomeContext
{
    private bool isDisposed = false;
    public void Dispose()
    {
        Console.WriteLine("Dispose being called on Context");
        isDisposed = true;
    }

    public bool GetDisposed()
    {
        return isDisposed;
    }
}

服务注册

services.AddScoped<ISomeContext, SomeContext>();
services.AddScoped<IUnitOfWork, UnitOfWork>();

控制器逻辑

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IUnitOfWork _uow;
    public ValuesController(IUnitOfWork uow)
    {
        _uow = uow;
    }
        
    [Route("Test")]
    public async Task<IActionResult> GetTest()
    {
        Task myTask = SomeLongRunningThing();
        try
        {
            return Ok();
        }
        finally
        {
            Response.OnCompleted(async () =>
            {
                Task continueTask = myTask.ContinueWith(async (task) =>
                {
                    await Task.Delay(5000);
                    Console.WriteLine("Checking if context is disposed...");
                    Console.WriteLine(_uow.GetContextDisposed());
                });
                await myTask;
                await continueTask;
            });
        }
        Console.WriteLine("End of route");
    }

    private Task SomeLongRunningThing()
    {
        return Task.Run(() =>
        {
            Task.Delay(10000);
        });
    }
}

运行输出

Dispose being called on Context
Dispose being called on Context
Dispose being called on Context
Dispose being called on Context
Checking if context is disposed...
True

根本原因

  1. ASP.NET Core 中Scoped生命周期的服务,生命周期绑定到当前请求对应的独立IServiceScope。框架会在请求主处理管道执行完成(即Action方法返回结果、响应开始向客户端发送的节点),立刻释放该请求的Scope,所有Scope内注册的Scoped服务会被批量触发Dispose,这个释放时机和Response.OnCompleted回调的执行没有绑定关系。
  2. Response.OnCompleted的回调触发时机是响应内容完全发送到客户端之后,此时请求对应的Scope已经被释放,回调中直接引用控制器注入的Scoped服务实例,自然会得到已释放的状态。
  3. 代码中额外存在两个逻辑问题:
    • SomeLongRunningThing方法内的Task.Delay(10000)没有await,实际执行时会立刻返回,10秒延迟不会生效
    • ContinueWith没有指定任务调度器,回调执行的线程上下文不可控,容易出现跨线程访问请求对象的异常

解决方案

  • 不要在Response.OnCompleted这类脱离请求主管道的回调中,直接使用请求Scope注入的Scoped服务。如果需要在响应发送后执行后续逻辑,需要手动创建独立的服务Scope,从新Scope中解析所需服务,用完后自行释放:
Response.OnCompleted(async () =>
{
    // 创建独立服务Scope,和原请求Scope生命周期隔离
    using var independentScope = HttpContext.RequestServices.CreateScope();
    // 从新Scope解析服务,不要复用控制器注入的_uow
    var uow = independentScope.ServiceProvider.GetRequiredService<IUnitOfWork>();
    
    await myTask;
    await Task.Delay(5000);
    Console.WriteLine("Checking if context is disposed...");
    Console.WriteLine(uow.GetContextDisposed()); // 返回False,服务会在using代码块结束后才释放
});
  • 如果是需要执行超过数秒的长耗时任务,不要放在请求回调中执行,应该实现后台任务队列,将任务投递到IHostedService实现的托管后台服务中执行,完全脱离请求生命周期,避免请求回收导致任务中断、对象释放问题。
  • 禁止为了规避释放问题将服务生命周期改为Singleton,会导致数据库上下文等有状态服务被所有请求共享,引发并发冲突、数据脏读等严重问题。

内容的提问来源于stack exchange,提问作者Jeremy Farmer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 21:42:17