Правильный способ использования 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
, Я рекомендую настройку httpRuntime
targetFramework
в 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
и т.п.
Вы все равно не хотите получать доступ к общему состоянию в другом потоке, и с Task
s, вам редко нужно обращаться с этим явно - просто убедитесь, что вы вводите все свои входные данные в качестве аргументов и только возвращаете ответ, а не читаете или пишете в общее состояние (например, HttpContext
статические поля и т. д.)
Там нет проблем, если ваш ViewModels.DisplayChannel
это простой объект без дополнительной логики.
Проблема может возникнуть, если результат вашего Task
ссылки на "некоторые объекты контекста", например HttpContext.Current
, Такие объекты часто присоединяются к потоку, но весь код после await
может быть выполнен в другом потоке.
Имейте в виду, что UseTaskFriendlySynchronizationContext
не решает все ваши проблемы. Если мы говорим о ASP.NET MVC, этот параметр гарантирует, что Controller.HttpContext
содержит правильное значение, как и раньше await
как после. Но это не гарантирует, что HttpContext.Current
содержит правильное значение, а после await
это все еще может быть нулем.