web-dev-qa-db-fra.com

Attendre correctement l'utilisation de HttpContext.Current.User avec async

Je travaille avec des actions asynchrones et j'utilise HttpContext.Current.User comme ceci

public class UserService : IUserService
{
   public ILocPrincipal Current
   {
       get { return HttpContext.Current.User as ILocPrincipal; }
   }
}

public class ChannelService : IDisposable
{
    // In the service layer 
    public ChannelService()
          : this(new Entities.LocDbContext(), new UserService())
      {
      }

    public ChannelService(Entities.LocDbContext locDbContext, IUserService userService)
    {
      this.LocDbContext = locDbContext;
      this.UserService = userService;
    }

    public async Task<ViewModels.DisplayChannel> FindOrDefaultAsync(long id)
    {
     var currentMemberId = this.UserService.Current.Id;
     // do some async EF request …
    }
}

// In the controller
[Authorize]
[RoutePrefix("channel")]
public class ChannelController : BaseController
{
    public ChannelController()
        : this(new ChannelService()
    {
    }

    public ChannelController(ChannelService channelService)
    {
        this.ChannelService = channelService;
    }

    // …

    [HttpGet, Route("~/api/channels/{id}/messages")]
    public async Task<ActionResult> GetMessages(long id)
    {
        var channel = await this.ChannelService
            .FindOrDefaultAsync(id);

        return PartialView("_Messages", channel);
    }

    // …
}

J'ai récemment refactorisé le code, auparavant je devais donner à l'utilisateur à chaque appel au service. Maintenant, je lis cet article http://trycatchfail.com/blog/post/Using-HttpContext-Safely-After-Async-in-ASPNET-MVC-Applications.aspx et je ne sais pas si mon code fonctionne toujours. Quelqu'un at-il une meilleure approche pour gérer cela? Je ne veux pas donner à l'utilisateur à chaque demande au service.

32
Magu

Tant que vos paramètres web.config Sont corrects , async/await fonctionne parfaitement avec HttpContext.Current. Je recommande de définir httpRuntimetargetFramework sur 4.5 Pour supprimer tout comportement en "mode excentrique".

Une fois cela fait, plain async/await fonctionnera parfaitement bien. Vous ne rencontrerez des problèmes que si vous travaillez sur un autre thread ou si votre code await est incorrect.


Tout d'abord, le problème des "autres threads"; c'est le deuxième problème dans le blog auquel vous avez lié. Un code comme celui-ci ne fonctionnera bien sûr pas correctement:

async Task FakeAsyncMethod()
{
  await Task.Run(() =>
  {
    var user = _userService.Current;
    ...
  });
}

Ce problème n'a en fait rien à voir avec le code asynchrone; cela concerne la récupération d'une variable de contexte à partir d'un thread de pool de threads (sans demande). Le même problème se produirait exactement si vous essayez de le faire de manière synchrone.

Le problème principal est que la version asynchrone utilise faux asynchrone. Cela est inapproprié, en particulier sur ASP.NET. La solution consiste simplement à supprimer le code faux-asynchrone et à le rendre synchrone (ou vraiment asynchrone, s'il a réellement un vrai travail asynchrone à faire):

void Method()
{
  var user = _userService.Current;
  ...
}

La technique recommandée dans le blog lié (envelopper le HttpContext et en le fournissant au thread de travail) est extrêmement dangereuse. HttpContext est conçu pour être accessible uniquement à partir d'un thread à la fois et AFAIK n'est pas du tout threadsafe. Donc, le partager entre différents fils, c'est demander un monde de souffrance.


Si le code await est incorrect, il provoque un problème similaire. ConfigureAwait(false) est une technique couramment utilisée dans le code de bibliothèque pour notifier au runtime qu'il n'a pas besoin de revenir à un contexte spécifique. Considérez ce code:

async Task MyMethodAsync()
{
  await Task.Delay(1000).ConfigureAwait(false);
  var context = HttpContext.Current;
  // Note: "context" is not correct here.
  // It could be null; it could be the correct context;
  //  it could be a context for a different request.
}

Dans ce cas, le problème est évident. ConfigureAwait(false) indique à ASP.NET que le reste de la méthode actuelle n'a pas besoin du contexte, puis il accède immédiatement à ce contexte. Cependant, lorsque vous commencez à utiliser des valeurs de contexte dans vos implémentations d'interface, le problème n'est pas aussi évident:

async Task MyMethodAsync()
{
  await Task.Delay(1000).ConfigureAwait(false);
  var user = _userService.Current;
}

Ce code est tout aussi faux mais pas aussi manifestement faux, car le contexte est caché derrière une interface.

Ainsi, la ligne directrice générale est la suivante: utilisez ConfigureAwait(false) si vous savez que la méthode ne dépend pas de son contexte ( directement ou indirectement); sinon, n'utilisez pas ConfigureAwait. S'il est acceptable dans votre conception que les implémentations d'interface utilisent le contexte dans leur implémentation, alors toute méthode qui appelle une méthode d'interface doit pas utiliser ConfigureAwait(false):

async Task MyMethodAsync()
{
  await Task.Delay(1000);
  var user = _userService.Current; // works fine
}

Tant que vous suivez cette directive, async/await fonctionnera parfaitement avec HttpContext.Current.

54
Stephen Cleary

Async va bien. Le problème est lorsque vous publiez le travail sur un autre thread. Si votre application est configurée en 4.5+, le rappel asynchrone sera publié dans le contexte d'origine, vous aurez donc également la bonne HttpContext etc.

De toute façon, vous ne voulez pas accéder à l'état partagé dans un thread différent, et avec Tasks, vous avez rarement besoin de gérer cela explicitement - assurez-vous simplement de mettre toutes vos entrées en arguments, et de renvoyer uniquement une réponse, plutôt que de lire ou d'écrire dans un état partagé (par exemple HttpContext, champs statiques, etc.)

2
Luaan

Il n'y a aucun problème si votre ViewModels.DisplayChannel est un objet simple sans logique supplémentaire.

Un problème peut se produire si le résultat de vos références Task à "certains objets de contexte", par exemple. à HttpContext.Current. Ces objets sont souvent attachés au thread, mais le code entier après await peut être exécuté dans un autre thread.

Gardez à l'esprit que UseTaskFriendlySynchronizationContext ne résout pas tous vos problèmes. Si nous parlons d'ASP.NET MVC, ce paramètre garantit que Controller.HttpContext contient la valeur correcte comme avant await comme après. Mais cela ne garantit pas que HttpContext.Current contient la valeur correcte, et après await, elle peut toujours être null.

2
Mark Shevchenko