Session.SetString 方法在 ASP.NET Core 3.1 中抛出异常 "IFeatureCollection has been disposed. Object name: 'Collection'. "

Session.SetString method throws exception "IFeatureCollection has been disposed. Object name: 'Collection'. " in ASP.NET Core 3.1

我有一个用 ASP.NET Core 3.1 编写的项目。

我需要在Singleton服务中给Session设置数据:

 _session.SetString("some key", "some value");

我从 DI 中注入了会话对象:

public OperatorService(ILogger<OperatorService> logger, 
                       ISession session,
                       IOptions<AppSettings> options)
{
     this._session = session; 
     this._logger = logger; 
     this._appSettings = options.Value;
}

我调用我的方法如下:

public void ChangeOperatorStatus(StatusChangeRequest request)
{
     try
     {
         _session.SetString(request.Key, request.Value);
     }
     catch (Exception ex)
     {
          _logger.LogInformation($"Exception while changing status: {ex}"); 
     } 
} 

但我得到以下异常:

IFeatureCollection has been disposed.\r\nObject name: 'Collection'. 

并且我在 Startup.cs 的 ConfigureServices 方法中添加了一些代码:

services.AddHttpContextAccessor();

services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(20);
    options.Cookie.HttpOnly = true;
})
.AddDistributedMemoryCache();

并且我在 Startup.cs 的 Configure 方法中添加了 app.UseSession();

我尝试了 services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); 并从 httpContextAccessor.HttpContext.Session 获得了会话,但我得到了同样的错误。

请帮帮我,谢谢。

HttpContext 是单个请求的上下文。它提供对该单个请求的请求、响应属性等的访问。您无法缓存它,一旦该请求结束,它就会失效。

会话是另一个短暂的东西——它只存在一个用户会话。网络应用程序的每个用户至少有一个会话。在单例中缓存其中一个会话可以保证

  • 引用将在一段时间后失效,当会话过期并且
  • 单例将只使用该用户的值,而忽略其他人的值。这本身就是一个错误,也是侵入应用程序的 绝妙 方法。
  • 如果管理员登录,会话对象可能会在接下来的 20、30 或 60 分钟内将管理员的设置应用到每个人。

这就是为什么使用会话对每个请求中间件有意义,而不是单例服务。

HttpContext的正确用法

Session 只能通过请求的上下文到达,因此获得正确的会话意味着获得正确的 HttpContext。 David Fowler's ASP.NET Core Guidance 中解释了正确的方法:

❌ BAD This example stores the HttpContext in a field then attempts to use it later.

    private readonly HttpContext _context;
    public MyType(IHttpContextAccessor accessor)
    {
        _context = accessor.HttpContext;
    }
    public void CheckAdmin()
    {
        if (!_context.User.IsInRole("admin"))
        {
            throw new UnauthorizedAccessException("The current user isn't an admin");
        }
    }

✅ GOOD This example stores the IHttpContextAccesor itself in a field and uses the HttpContext field at the correct time (checking for null).

   private readonly IHttpContextAccessor _accessor;
   public MyType(IHttpContextAccessor accessor)
   {
       _accessor = accessor;
   }

   public void CheckAdmin()
   {
       var context = _accessor.HttpContext;
       if (context != null && !context.User.IsInRole("admin"))
       {
           throw new UnauthorizedAccessException("The current user isn't an admin");
       }
   }

改为使用 Scoped 服务

因为 Singleton 不知道要使用哪个会话。一种选择是将该服务简单地转换为 Scoped 服务。在 ASP.NET Core 中,一个请求定义了一个范围。这就是控制器操作和管道中间件如何为每个请求访问正确的 HttpContext。

假设该服务被一个动作或中间件使用,也许唯一需要的改变是将 AddSingleton<ThatService> 替换为 AddScoped<ThatService>

扭转局面,或控制反转

另一个选项是 调用者 应该向它提供会话。而不是使用缓存会话,例如:

public void SetStatus(string status)
{
    _session.SetString(SessionKeys.UserStatus, "some value");
}

请求会话或 HttpContext 作为参数:

public void SetStatus(string status,ISession session)
{
    session.SetString(SessionKeys.UserStatus, "some value");
}

并让呼叫者将正确的会话传递给它

我花了一段时间才解决这个问题。 在我的例子中,它是一个 3.1 aspnetcore,直到我将容器函数从

public async void OnPost

public async Task<IActionResult> OnPost

看起来 HttpContext 在使用之前就被释放了...