Правильный способ использования HttpContext.Current.User с асинхронным ожиданием

Я работаю с асинхронными действиями и использую HttpContext.Current.User, как это

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);
    }

    // …
}

Я недавно переделал код, ранее мне приходилось давать пользователю при каждом обращении в сервис. Сейчас я читаю эту статью http://trycatchfail.com/blog/post/Using-HttpContext-Safely-After-Async-in-ASPNET-MVC-Applications.aspx и не уверен, что мой код все еще работает. Кто-нибудь лучше подход к этому? Я не хочу давать пользователю на каждый запрос к службе.

3 ответа

Решение

Пока ваш web.config настройки верны, async / await отлично работает с HttpContext.Current, Я рекомендую настройку httpRuntimetargetFramework в 4.5 удалить все "причудливое" поведение.

Как только это будет сделано, просто async / await будет работать на отлично Проблемы возникнут только в том случае, если вы выполняете работу в другом потоке или если ваш await Код указан неверно.


Во-первых, проблема "другого потока"; это вторая проблема в блоге, на который вы ссылаетесь. Такой код, конечно, не будет работать правильно:

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

Эта проблема на самом деле не имеет ничего общего с асинхронным кодом; это связано с извлечением переменной контекста из потока пула потоков (не по запросу). Точно такая же проблема возникнет, если вы попытаетесь сделать это синхронно.

Основная проблема заключается в том, что асинхронная версия использует ложную асинхронность. Это неуместно, особенно на ASP.NET. Решение состоит в том, чтобы просто удалить фальшивый асинхронный код и сделать его синхронным (или действительно асинхронным, если он действительно должен выполнять реальную асинхронную работу):

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

Техника, рекомендованная в связанном блоге HttpContext и предоставление его рабочему потоку) чрезвычайно опасно. HttpContext предназначен для доступа только из одного потока за раз, и AFAIK не является потокобезопасным вообще. Так что делиться этим между разными темами - это мир больно.


Если await код неверный, то это вызывает аналогичную проблему. ConfigureAwait(false) это метод, обычно используемый в коде библиотеки для уведомления среды выполнения о том, что ей не нужно возвращаться в определенный контекст. Рассмотрим этот код:

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.
}

В этом случае проблема очевидна. ConfigureAwait(false) сообщает ASP.NET, что остальной части текущего метода не нужен контекст, а затем он немедленно обращается к этому контексту. Однако когда вы начинаете использовать значения контекста в реализациях вашего интерфейса, проблема не столь очевидна:

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

Этот код такой же неправильный, но не такой явно неправильный, поскольку контекст скрыт за интерфейсом.

Итак, общее руководство: использовать ConfigureAwait(false) если вы знаете, что метод не зависит от его контекста (прямо или косвенно); в противном случае не используйте ConfigureAwait, Если в вашем проекте допустимо, чтобы реализации интерфейса использовали контекст в своей реализации, то любой метод, который вызывает метод интерфейса, не должен использовать ConfigureAwait(false):

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

Пока вы следуете этому руководству, async / await будет отлично работать с HttpContext.Current,

Async в порядке. Проблема в том, когда вы публикуете работу в другой ветке. Если ваше приложение настроено на 4.5+, асинхронный обратный вызов будет опубликован в исходном контексте, поэтому вы также будете иметь HttpContext и т.п.

Вы все равно не хотите получать доступ к общему состоянию в другом потоке, и с Tasks, вам редко нужно обращаться с этим явно - просто убедитесь, что вы вводите все свои входные данные в качестве аргументов и только возвращаете ответ, а не читаете или пишете в общее состояние (например, HttpContextстатические поля и т. д.)

Там нет проблем, если ваш ViewModels.DisplayChannel это простой объект без дополнительной логики.

Проблема может возникнуть, если результат вашего Task ссылки на "некоторые объекты контекста", например HttpContext.Current, Такие объекты часто присоединяются к потоку, но весь код после await может быть выполнен в другом потоке.

Имейте в виду, что UseTaskFriendlySynchronizationContext не решает все ваши проблемы. Если мы говорим о ASP.NET MVC, этот параметр гарантирует, что Controller.HttpContext содержит правильное значение, как и раньше await как после. Но это не гарантирует, что HttpContext.Current содержит правильное значение, а после await это все еще может быть нулем.

Другие вопросы по тегам