在 WebApi 中使用 HttpContext.Current 是危险的,因为它与异步有关。

69

我的问题与这个有点相关:WebApi中使用HttpContext.Current和Ninject实现依赖注入的等效方法

我们希望在WebApi区域使用HttpContext.Current和Ninject注入一个类。

我担心这可能是非常危险的,因为在WebApi (所有内容?)都是异步的。

请纠正我,如果我在以下几个方面上有错误,这是我迄今为止调查的结果:

  1. HttpContext.Current通过Thread获取当前上下文(我直接查看了实现)。

  2. 在异步任务中使用HttpContext.Current是不可能的,因为它可以在另一个线程上运行。

  3. WebApi使用IHttpController和方法Task<HttpResponseMessage> ExecuteAsync =>每个请求都是异步的 =>您不能在操作方法内部使用HttpContext.Current。甚至可能发生多个请求在同一线程上执行的巧合。

  4. 为了构建具有注入到构造函数中的控制器,使用的是IHttpControllerActivator,其中包含同步方法IHttpController Create,这是ninject创建Controller及其依赖项的位置。


  • 如果我在这四个点上都正确,那么在操作方法或任何位于下层的层中使用HttpContext.Current是非常危险且会产生意想不到的结果的。我在StackOverflow上看到很多被接受的答案正是建议这样做的。我认为这可能在一段时间内能够工作,但在负载下将失败。

  • 但是,当使用DI创建控制器及其依赖项时,它是可以的,因为它只运行在一个独立的线程上。 我可以在构造函数中获取HttpContext的值,这种方式是安全的吗?我想知道每个控制器是否在每个请求上都是在单个线程上创建的,因为这可能会在负载高时导致问题,其中所有IIS线程都可能被消耗。

只是为了解释我为什么想要注入HttpContext相关内容:

  • 一种解决方案是在控制器操作方法中获取请求并将所需值作为参数传递到所有层,直到它在代码的某处深处使用。
  • 我们想要的解决方案: 在此之间的所有层都不受影响,并且我们可以在代码的深处使用注入的请求 (例如,在某些依赖于URL的 ConfigurationProvider 中)。

如果我的想法完全错误或者我的建议是正确的,请给我您的意见,因为这个主题似乎非常复杂。


这是一个非常有趣的问题。然而,据我所知,如果代码没有明确地将工作推迟到另一个线程,那么任务仍将在同一线程上执行。对于异步任务也是如此。虽然它不会阻塞调用者线程,但它仍将在调用者线程上执行。但我不是100%确定...你应该试一试;-) - BatteryBackupUnit
这正是问题所在 - 使用await时,大多数情况下您会停留在同一线程上,但不能确定。这就是为什么我认为每个人都认为它有效,但在某些情况下会出现问题的原因。使用Task.Run,可以确定它是在另一个线程上执行的。 - Lukas K
为什么你认为这不确定呢?比使用“同步”方法更不确定吗?“同步”方法也可以执行Task.Run(..),或者它可以创建一个新线程,在该线程中尝试获取注入的HttpContext..等等。那么有什么区别呢? - BatteryBackupUnit
当你使用Task.Run时,你自己知道要做什么并且知道可以期望什么,这对我来说没问题。我的担忧是,由于WebApi中的操作方法是由框架异步运行的,因此它始终应被视为具有所有缺陷的异步操作。我认为,甚至可能发生两个请求在同一个线程上运行(我认为这就是await/async的目的)=> HttpContext.Current将返回错误的值。实际上,这个问题已经让我遇到过一次,我意识到在WebApi中记录日志时,log4net LogicalThreadContext.Properties不起作用,但在MVC中它们确实起作用。 - Lukas K
回答你的问题,为什么我认为它不确定是否在同一线程上运行? - 我在互联网上阅读了许多文章,似乎社区对此并不确定,但我有一种感觉,即同步上下文将根据当前情况决定是否启动新线程,但实际上这种情况非常罕见。(仍然不确定100%,因为互联网上有太多不同的意见) - Lukas K
3个回答

116

HttpContext.Current通过线程获取当前上下文(我直接查看了实现)。

更准确的说,HttpContext应用于一个线程;或者说一个线程“进入”HttpContext

在异步任务中使用HttpContext.Current是不可能的,因为它可能在另一个线程上运行。

完全不是这样的;async/await的默认行为将在任意线程上恢复,但该线程将在恢复您的async方法之前进入请求上下文。


关键在于SynchronizationContext。如果您不熟悉它,我有一篇MSDN文章介绍。一个SynchronizationContext为平台定义了一个“上下文”,常见的包括UI上下文(WPF、WinPhone、WinForms等)、线程池上下文和ASP.NET请求上下文。

ASP.NET请求上下文管理HttpContext.Current以及其他一些东西,比如区域设置和安全性。UI上下文都与一个线程紧密关联(唯一的UI线程),但ASP.NET请求上下文并不绑定到特定的线程。但它只允许一个线程同时处于请求上下文中。

解决方案的另一部分是 asyncawait 如何工作。我在博客中有一篇介绍它们行为的 async简介。简而言之,await 默认会捕获当前上下文(除非它是null,这个上下文通常是 SynchronizationContext.Current),并使用该上下文来恢复async 方法。因此,await 自动捕获 ASP.NET 的SynchronizationContext 并在请求上下文中恢复async 方法(从而保留了区域设置、安全性和 HttpContext.Current)。
如果你使用 ConfigureAwait(false),那么你明确告诉 await 不要捕获上下文。
需要注意的是,ASP.NET 必须改变其 SynchronizationContext 才能与 async/await 洁净地工作。你必须确保应用程序编译为 .NET 4.5,并且在web.config中也明确选定4.5版本。这是新的 ASP.NET 4.5 项目的默认值,但如果你将现有项目从 ASP.NET 4.0 或更早版本升级,则必须明确设置。
你可以通过针对 .NET 4.5 执行应用程序并观察 SynchronizationContext.Current 来确保这些设置是正确的。如果它是 AspNetSynchronizationContext,那么就没问题;如果是 LegacyAspNetSynchronizationContext,那么设置就是错误的。
只要设置正确(并且你使用 ASP.NET 4.5 的 AspNetSynchronizationContext),那么你可以在await之后安全地使用 HttpContext.Current,无需担心它。

3
如果服务器端的怪异模式是活动状态,那么遗留上下文才会被使用。只有当您升级到.NET 4.5并且尚未关闭它时,才会出现这种情况。对于一个新的.NET 4.5 ASP.NET项目,默认情况下使用新的上下文。更多信息在此处 - Stephen Cleary
2
@Stephen,非常感谢您的出色回答。我必须读了两遍才意识到我所面临的问题是您提到的没有将“httpRuntime”设置为“4.5”的问题。一旦我这样做了,我的所有问题都消失了。 - Kirk Woll
1
@crokusek:如果你正确地针对.NET 4.5或更高版本进行目标设置,就不需要使用AsyncLocal<T>HttpContext.Current会在await点之间流动。 - Stephen Cleary
2
@Mick:每个方法都选择捕获和恢复自己的上下文。如果被调用的方法丢弃了它的上下文,那不会影响调用者。如果调用者方法在调用此方法后丢弃其上下文,则不会影响此方法。如果调用者方法在调用此方法之前丢弃其上下文,则此方法将没有上下文。 - Stephen Cleary
2
@Shawson:是的。这不是推荐的(它仍然被认为是一种糟糕的设计),但它可以工作。 - Stephen Cleary
显示剩余18条评论

6
我正在使用一个使用async/await方法的Web API。同时还在使用。
1) HttpContext.Current.Server.MapPath
2) System.Web.HttpContext.Current.Request.ServerVariables

这段时间一直运行正常,但突然出现问题,没有进行任何代码更改。

花费了很长时间回滚到之前的旧版本,最终发现缺失的密钥导致了问题。

< httpRuntime targetFramework="4.5.2"  /> under system.web

虽然我在技术方面不是专家,但我建议您将密钥添加到您的Web配置文件中,并开始使用它。


感谢您的评论。这实际上与Stephen的接受答案相对应。在新框架中,同步上下文已经重新设计,并且他们确保我提到的问题正如预期地正确工作。 - Lukas K

2
我找到了一篇非常好的文章,准确地描述了这个问题:http://byterot.blogspot.cz/2012/04/aspnet-web-api-series-part-3-async-deep.html?m=1 作者深入研究了WebApi框架中ExecuteAsync方法的调用方式,并得出结论:
ASP.NET Web API操作(以及所有管道方法)仅在返回Task或Task<T>时才异步调用。这听起来很明显,但是没有任何带有Async后缀的管道方法会在其自己的线程中运行。使用Async可能是一个误称。(更新:ASP.NET团队确实确认Async用于表示返回Task并可以异步运行但不必须)
我从这篇文章中理解到,Action方法是同步调用的,但这是调用者的决定。
我为此创建了一个小型测试应用程序,类似于以下内容:
public class ValuesController : ApiController
{
    public object Get(string clientId, string specialValue)
    {
        HttpRequest staticContext = HttpContext.Current.Request;
        string staticUrl = staticContext.Url.ToString();

        HttpRequestMessage dynamicContext = Request;
        string dynamicUrl = dynamicContext.RequestUri.ToString();

        return new {one = staticUrl, two = dynamicUrl};
    }
}

还有一个异步版本,返回async Task<object>

我试图用jquery对它进行一些DOS攻击,直到我使用了await Task.Delay(1).ConfigureAwait(false);,才发现问题所在。

我从文章中得出的结论是,当使用异步操作方法时,线程切换可能会发生问题,因此绝对不应该在从操作方法调用的代码中使用HttpContext.Current。但由于控制器是同步创建的,在构造函数和依赖注入中使用HttpContext.Current也是可以的。

如果有人对这个问题有另外的解释,请纠正我,因为这个问题非常复杂,我仍然没有100%的把握。

免责声明:

  • 我暂时忽略了自托管Web-Api没有IIS的问题,其中HttpContext.Current可能无法正常工作。我们现在依赖于IIS。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接