Why is an "await Task.Yield()" required for Thread.CurrentPrincipal to flow correctly?
Asked Answered
G

1

40

The code below was added to a freshly created Visual Studio 2012 .NET 4.5 WebAPI project.

I'm trying to assign both HttpContext.Current.User and Thread.CurrentPrincipal in an asynchronous method. The assignment of Thread.CurrentPrincipal flows incorrectly unless an await Task.Yield(); (or anything else asynchronous) is executed (passing true to AuthenticateAsync() will result in success).

Why is that?

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[] {})
            {
            }
        }
    }
}

Notes:

  • The await Task.Yield(); can appear anywhere in AuthenticateAsync() or it can be moved into GetAsync() after the call to AuthenticateAsync() and it will still succeed.
  • ApiController.User returns Thread.CurrentPrincipal.
  • HttpContext.Current.User always flows correctly, even without await Task.Yield().
  • Web.config includes <httpRuntime targetFramework="4.5"/> which implies UseTaskFriendlySynchronizationContext.
  • I asked a similar question a couple days ago, but did not realize that example was only succeeding because Task.Delay(1000) was present.
Gazehound answered 20/5, 2013 at 15:49 Comment(3)
What exactly happens if you leave that out?Turtledove
@SLaks, If await Task.Yield() is skipped, Thread.CurrentPrincipal reverts back to what it was before calling await AuthenticateAsync(). Since Thread.CurrentPrincipal is no longer a MyPrincipal, the exception is thrown.Gazehound
In my Owin middleware, I had to chain in a last piece of middleware that simply does await Task.Yield(); this seems to cause Thread.CurrentPrincipal to be as expected later throughout the execution.Pepillo
A
44

How interesting! It appears that Thread.CurrentPrincipal is based on the logical call context, not the per-thread call context. IMO this is quite unintuitive and I'd be curious to hear why it was implemented this way.


In .NET 4.5., async methods interact with the logical call context so that it will more properly flow with async methods. I have a blog post on the topic; AFAIK that's the only place where it's documented. In .NET 4.5, at the beginning of every async method, it activates a "copy-on-write" behavior for its logical call context. When (if) the logical call context is modified, it will create a local copy of itself first.

You can see the "localness" of the logical call context (i.e., whether it has been copied) by observing System.Threading.Thread.CurrentThread.ExecutionContextBelongsToCurrentScope in a watch window.

If you don't Yield, then when you set Thread.CurrentPrincipal, you're creating a copy of the logical call context, which is treated as "local" to that async method. When the async method returns, that local context is discarded and the original context takes its place (you can see ExecutionContextBelongsToCurrentScope returning to false).

On the other hand, if you do Yield, then the SynchronizationContext behavior takes over. What actually happens is that the HttpContext is captured and used to resume both methods. In this case, you're not seeing Thread.CurrentPrincipal preserved from AuthenticateAsync to GetAsync; what is actually happening is HttpContext is preserved, and then HttpContext.User is overwriting Thread.CurrentPrincipal before the methods resume.

If you move the Yield into GetAsync, you see similar behavior: Thread.CurrentPrincipal is treated as a local modification scoped to AuthenticateAsync; it reverts its value when that method returns. However, HttpContext.User is still set correctly, and that value will be captured by Yield and when the method resumes, it will overwrite Thread.CurrentPrincipal.

Atavism answered 20/5, 2013 at 16:52 Comment(2)
Hi! Did you ever heard why this was implemented this way? I have read this post 3 times already and it is still blowing my mind.Sinnard
@vtortola: I don't know for sure. I assume it was so that the user permissions would flow to background threads automatically. That was probably done a good decade ago, and the copy-context-on-update behavior is much more recent. So they conflicted in this odd way.Atavism

© 2022 - 2024 — McMap. All rights reserved.