Por que um “await Task.Yield ()” é necessário para o Thread.CurrentPrincipal fluir corretamente?

O código abaixo foi adicionado a um projeto WebAPI do Visual Studio 2012 .NET 4.5 recém-criado.

Eu estou tentando atribuir HttpContext.Current.User e Thread.CurrentPrincipal em um método asynchronous. A atribuição de Thread.CurrentPrincipal flui incorretamente, a menos que um await Task.Yield(); (ou qualquer outra coisa assíncrona) é executada (passar true para AuthenticateAsync() resultará em sucesso).

Por que é que?

 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(""), new string[] {}) { } } } } 

Notas:

  • O await Task.Yield(); pode aparecer em qualquer lugar em AuthenticateAsync() ou pode ser movido para GetAsync() após a chamada para AuthenticateAsync() e ainda terá êxito.
  • ApiController.User retorna Thread.CurrentPrincipal .
  • HttpContext.Current.User sempre flui corretamente, mesmo sem await Task.Yield() .
  • Web.config inclui que implica em UseTaskFriendlySynchronizationContext .
  • Eu fiz uma pergunta parecida alguns dias atrás, mas não percebi que o exemplo só estava tendo sucesso porque Task.Delay(1000) estava presente.

Que interessante! Parece que Thread.CurrentPrincipal é baseado no contexto de chamada lógica , não no contexto de chamada por thread. IMO isso não é muito intuitivo e eu ficaria curioso para saber por que foi implementado desta forma.


No .NET 4.5, os methods async interagem com o contexto da chamada lógica para que ele flua mais corretamente com os methods async . Eu tenho um post no blog sobre o assunto ; AFAIK é o único lugar onde é documentado. No .NET 4.5, no início de cada método async , ele ativa um comportamento “copy-on-write” para seu contexto de chamada lógica. Quando (se) o contexto da chamada lógica for modificado, ele criará primeiro uma cópia local de si mesmo.

Você pode ver a “localidade” do contexto da chamada lógica (isto é, se foi copiada) observando System.Threading.Thread.CurrentThread.ExecutionContextBelongsToCurrentScope em uma janela de inspeção.

Se você não Yield , quando você definir Thread.CurrentPrincipal , estará criando uma cópia do contexto da chamada lógica, que é tratada como “local” para esse método async . Quando o método async retorna, esse contexto local é descartado e o contexto original toma o seu lugar (você pode ver ExecutionContextBelongsToCurrentScope retornando para false ).

Por outro lado, se você fizer Yield , o comportamento SynchronizationContext assumirá. O que realmente acontece é que o HttpContext é capturado e usado para retomar os dois methods. Nesse caso, você não GetAsync Thread.CurrentPrincipal preservado de AuthenticateAsync para GetAsync ; O que está realmente acontecendo é HttpContext é preservado e, em seguida, HttpContext.User está sobrescrevendo Thread.CurrentPrincipal antes de retomar os methods.

Se você mover o Yield para GetAsync , você verá um comportamento semelhante: Thread.CurrentPrincipal é tratado como uma modificação local com escopo para AuthenticateAsync ; ele reverte seu valor quando esse método retorna. No entanto, HttpContext.User ainda está definido corretamente, e esse valor será capturado pelo Yield e quando o método for retomado, ele replaceá Thread.CurrentPrincipal .