Правильный способ использования 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 и не уверен, работает ли мой код. У кого-нибудь есть лучший подход, чтобы справиться с этим? Я не хочу давать пользователю по каждому запросу услуги.

Ответ 1

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

Как только это будет сделано, обычная 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.

Ответ 2

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

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

Ответ 3

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

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

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