39

下面的代码已添加到新创建的 Visual Studio 2012 .NET 4.5 WebAPI 项目中。

我正在尝试以异步方法分配HttpContext.Current.User两者Thread.CurrentPrincipalThread.CurrentPrincipal除非执行await Task.Yield();(或其他任何异步操作)(传递trueAuthenticateAsync()将导致成功),否则流的分配不正确。

这是为什么?

using System.Security.Principal;
using System.Threading.Tasks;
using System.Web.Http;

namespace ExampleWebApi.Controllers
{
    public class ValuesController : ApiController
    {
        public async Task GetAsync()
        {
            await AuthenticateAsync(false);

            if (!(User is MyPrincipal))
            {
                throw new System.Exception("User is incorrect type.");
            }
        }

        private static async Task AuthenticateAsync(bool yield)
        {
            if (yield)
            {
                // Why is this required?
                await Task.Yield();
            }

            var principal = new MyPrincipal();
            System.Web.HttpContext.Current.User = principal;
            System.Threading.Thread.CurrentPrincipal = principal;
        }

        class MyPrincipal : GenericPrincipal
        {
            public MyPrincipal()
                : base(new GenericIdentity("<name>"), new string[] {})
            {
            }
        }
    }
}

笔记:

  • await Task.Yield();可以出现在任何地方,也AuthenticateAsync()可以GetAsync()在调用之后移动到AuthenticateAsync()它仍然会成功。
  • ApiController.User返回Thread.CurrentPrincipal
  • HttpContext.Current.User始终正确流动,即使没有await Task.Yield().
  • Web.config包括<httpRuntime targetFramework="4.5"/>意味着 UseTaskFriendlySynchronizationContext
  • 几天前我问了一个类似的问题Task.Delay(1000),但没有意识到这个例子只是因为存在而成功。
4

1 回答 1

44

很有意思!似乎这Thread.CurrentPrincipal是基于逻辑调用上下文,而不是每个线程调用上下文。IMO 这很不直观,我很想知道为什么要以这种方式实施。


在 .NET 4.5 中,async方法与逻辑调用上下文交互,以便它更正确地与async方法一起流动。我有一篇关于该主题的博客文章;AFAIK 这是唯一记录它的地方。在 .NET 4.5 中,在每个方法的开头async,它都会为其逻辑调用上下文激活“写时复制”行为。当(如果)逻辑调用上下文被修改时,它将首先创建自己的本地副本。

System.Threading.Thread.CurrentThread.ExecutionContextBelongsToCurrentScope您可以通过在监视窗口中观察来查看逻辑调用上下文的“本地性”(即,是否已被复制) 。

如果您不这样做Yield,那么当您设置 时Thread.CurrentPrincipal,您将创建逻辑调用上下文的副本,该副本被视为该async方法的“本地”。当async方法返回时,该本地上下文被丢弃,原始上下文取而代之(您可以看到ExecutionContextBelongsToCurrentScope返回到false)。

另一方面,如果你这样做了Yield,那么SynchronizationContext行为就会接管。实际发生的HttpContext是捕获并用于恢复这两种方法。在这种情况下,您不到从toThread.CurrentPrincipal保存的;实际发生的事情被保留,然后在方法恢复之前被覆盖。AuthenticateAsyncGetAsyncHttpContextHttpContext.UserThread.CurrentPrincipal

如果将Yieldinto移动GetAsync,您会看到类似的行为:Thread.CurrentPrincipal被视为范围为AuthenticateAsync;的本地修改。当该方法返回时,它会恢复其值。但是,HttpContext.User仍然设置正确,并且该值将被捕获Yield,当方法恢复时,它将覆盖Thread.CurrentPrincipal.

于 2013-05-20T16:52:47.460 回答