【问题标题】:Using HttpContext.Current in WebApi is dangerous because of async由于异步,在 WebApi 中使用 HttpContext.Current 很危险
【发布时间】:2014-09-17 08:01:56
【问题描述】:

我的问题与此有点相关:WebApi equivalent for HttpContext.Items with Dependency Injection

我们想使用 Ninject 在 WebApi 区域中使用 HttpContext.Current 注入一个类。

我担心的是,这可能非常危险,因为在 WebApi 中(一切?)都是异步的。

如果我在这些方面有错误,请纠正我,这是我目前调查的内容:

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

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

  3. WebApi 使用 IHttpController 和方法 Task<HttpResponseMessage> ExecuteAsync => 每个请求都是异步的 => 你不能在 action 方法中使用 HttpContext.Current。它甚至可能发生,更多的请求在同一个线程上执行。

  4. 为了在构造函数中注入东西来创建控制器 IHttpControllerActivatorsync 方法 IHttpController Create 一起使用。这就是 ninject 创建 Controller 及其所有依赖项的地方。


  • 如果我在所有这 4 点上都正确,那么在操作方法内部或下面的任何层中使用 HttpContext.Current 是非常危险的,并且可能会产生意想不到的结果。我在 StackOverflow 上看到了很多公认的答案,这些答案正是表明了这一点。在我看来,这可以工作一段时间,但会在负载下失败。

  • 但是当使用 DI 创建一个 Controller 及其依赖项时,没关系,因为它运行在一个单独的线程上。 我可以从构造函数中的 HttpContext 中获取一个值,这样会安全吗?。我想知道每个控制器是否针对每个请求都在单个线程上创建,因为这可能会在负载过重的情况下导致问题,其中 IIS 的所有线程都可能被消耗。

只是为了解释一下我为什么要注入 HttpContext 的东西:

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

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

【问题讨论】:

  • 这是一个非常有趣的问题。但是,据我所知,如果代码没有明确地将工作推迟到另一个线程,那么东西仍然会在同一个线程上执行。对于异步任务也是如此。虽然它不会阻塞调用者线程,但它仍然会在调用者线程上执行。但我不是 100% 确定.. 你应该试一试;-)
  • 这正是问题所在 - await 你大部分时间都停留在同一个线程上,但你不能确定这一点。这就是为什么 IMO 每个人都认为它有效,但在某些情况下会中断。使用 Task.Run,​​可以肯定的是,它是另一个线程。
  • 为什么你认为你不能确定呢?比“同步”方法更重要?同步方法也可以执行Task.Run(..) 或者它可以生成一个新线程并在该线程中尝试注入 HttpContext 等等。那么有什么区别?
  • 当你做Task.Run时,你自己做,你知道会发生什么,这对我来说没问题。我担心的是,由于 webapi 中的 action-method 是由框架异步运行的,所以它总是被认为是异步的,并且存在所有缺陷。我认为,它甚至可能发生,两个请求在同一个线程上运行(我认为这是等待/异步的目的)=> HttpContext.Current 将返回错误的值。实际上这个问题已经打击了我一次,通过登录 WebApi 意识到 log4net LogicalThreadContext.Properties 不起作用,但在 MVC 中它们确实起作用。
  • 回答你的问题,为什么我认为它不确定它是否在同一个线程上运行? - 我在互联网上阅读了很多文章,似乎社区对此不太确定,但我感觉 SyncronizationContext 将根据当前情况决定是否启动新线程,但实际上这种情况很少发生。 (仍然不确定 100% 关于这一点,因为互联网上有许多不同的意见)

标签: multithreading asynchronous asp.net-web-api ninject httpcontext


【解决方案1】:

我正在使用一个 web api,它使用 async/await 方法。

也使用

1) HttpContext.Current.Server.MapPath
2) System.Web.HttpContext.Current.Request.ServerVariables

这在很长一段时间内都可以正常工作,但由于没有更改代码而突然中断。

花费大量时间还原到以前的旧版本,发现丢失的密钥导致问题。

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

我不是技术专家。但我建议将密钥添加到您的网络配置中并给它一个 GO

【讨论】:

  • 感谢您的评论。这实际上与斯蒂芬接受的答案相对应。在新框架中,同步上下文被重新设计,他们确保我提到的那个问题按预期正常工作。
【解决方案2】:

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

HttpContext 应用于线程会更正确;或线程“进入”HttpContext

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

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


关键是SynchronizationContext。如果您不熟悉,我有一个MSDN article on the subjectSynchronizationContext 为平台定义了一个“上下文”,常见的是 UI 上下文(WPF、WinPhone、WinForms 等)、线程池上下文和 ASP.NET 请求上下文。

ASP.NET 请求上下文管理HttpContext.Current 以及其他一些东西,例如文化和安全性。 UI 上下文都与单个线程( UI 线程)紧密关联,但 ASP.NET 请求上下文不绑定到特定线程。但是,它一次只允许请求上下文中有一个线程。

解决方案的另一部分是asyncawait 如何工作。我有一个async intro on my blog that describes their behavior。总而言之,await 默认情况下将捕获当前上下文(即SynchronizationContext.Current,除非它是null),并使用该上下文来恢复async 方法。因此,await 会自动捕获 ASP.NET SynchronizationContext 并将在该请求上下文中恢复 async 方法(从而保持文化、安全和 HttpContext.Current)。

如果您awaitConfigureAwait(false),那么您明确告诉await不要捕获上下文。

请注意,ASP.NET 确实必须更改其 SynchronizationContext 才能与 async/await 完美配合。您必须确保应用程序是针对 .NET 4.5 和 also explicitly targets 4.5 in its web.config 编译的;这是新 ASP.NET 4.5 项目的默认设置,但如果您从 ASP.NET 4.0 或更早版本升级现有项目,则必须明确设置。

您可以通过针对 .NET 4.5 执行应用程序并观察 SynchronizationContext.Current 来确保这些设置正确无误。如果是AspNetSynchronizationContext,那么你很好;如果是LegacyAspNetSynchronizationContext,则设置错误。

只要设置正确(并且您使用的是 ASP.NET 4.5 AspNetSynchronizationContext),那么您就可以放心地在 await 之后使用 HttpContext.Current 而无需担心。

【讨论】:

  • @Stephen 您提到如果您获得的是旧版,则设置错误,但这似乎是默认设置。如果您想获得“非传统” SynchronizationContext,您必须将其添加到您的 Web.Config:
  • @ScottGartner:遗留上下文仅在服务器端怪癖模式处于活动状态时使用。仅当您升级到 .NET 4.5 并且尚未将其关闭时才会出现这种情况。对于新的 .NET 4.5 ASP.NET 项目,使用新上下文是默认设置。 More info here.
  • @Stephen,感谢这个了不起的答案。在我意识到我面临的问题是你提到的没有将httpRuntime 设置为4.5 之前,我必须阅读它两次。一旦我这样做了,我所有的问题都消失了。
  • @Mick:每种方法都选择捕获和恢复自己的上下文。如果被调用的方法丢弃其上下文,则不会影响调用者。如果调用方方法调用此方法后丢弃其上下文,则不会影响此方法。如果调用方方法在调用此方法之前丢弃其上下文,则此方法将没有上下文。
  • @Shawson:是的。这不是推荐(它仍然被认为是一个糟糕的设计),但它会起作用。
【解决方案3】:

我找到了很好的文章来描述这个问题:http://byterot.blogspot.cz/2012/04/aspnet-web-api-series-part-3-async-deep.html?m=1

作者深入研究了WebApi框架中ExecuteAsync方法是如何被调用的,得出了这样的结论:

仅当您返回 Task 或 Task 时,ASP.NET Web API 操作(以及所有管道方法)才会被异步调用。这听起来可能很明显,但是带有 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&lt;object&gt;

我尝试使用 jquery 对其进行一点 DOS 攻击,但在使用 await Task.Delay(1).ConfigureAwait(false); 之前无法确定任何问题,这显然会失败。

我从文章中得到的是,问题非常复杂,使用异步操作方法时可能会发生线程切换,因此在任何地方使用 HttpContext.Current 绝对是不是一个好主意从操作方法调用的代码。但是由于控制器是同步创建的,所以在构造函数中使用 HttpContext.Current 以及在依赖注入中也是可以的。

如果有人对此问题有其他解释,请纠正我,因为这个问题非常复杂,我仍然不是 100% 相信。

免责声明:

  • 我现在忽略了没有 IIS 的自托管 Web-Api 的问题,其中 HttpContext.Current 可能无论如何都无法工作。我们现在依赖 IIS。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多