正确使用HttpContext.Current.User与异步等待的方法

42

我正在使用异步操作,并像这样使用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);
    }

    // …
}

我最近对代码进行了重构,以前每次调用服务都需要给用户赋值。 现在我读了这篇文章 https://www.trycatchfail.com/2014/04/25/using-httpcontext-safely-after-async-in-asp-net-mvc-applications/ ,但我不确定我的代码是否仍然有效。 有人有更好的方法来处理这个问题吗? 我不想在每个服务请求中都给用户赋值。

3个回答

82
只要您的 web.config 设置正确, async/awaitHttpContext.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只能从一个线程访问,据我所知根本不是线程安全的。因此,在不同的线程之间共享它会带来许多麻烦。
如果 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
如果您想知道您的 ASP.NET 应用程序正在哪个运行时下运行,请设置断点并使用静态属性:HttpRuntime.TargetFramework - Dr Rob Lang

2
异步是没问题的。问题在于将工作提交到不同的线程中。如果您的应用程序设置为4.5+,异步回调将被发布在原始上下文中,因此您也将拥有适当的HttpContext等。
无论如何,您都不想在不同的线程中访问共享状态,并且使用Task时,您很少需要明确处理它 - 只需确保将所有输入作为参数放入,并仅返回响应,而不是读取或写入共享状态(例如HttpContext、静态字段等)。

2

如果您的ViewModels.DisplayChannel是一个简单的对象而没有额外的逻辑,那么就没有问题。

如果您的Task的结果引用了“一些上下文对象”,例如HttpContext.Current,则可能会出现问题。这些对象通常附加到线程上,但在await之后的整个代码可能在另一个线程中执行。

请记住,UseTaskFriendlySynchronizationContext不能解决所有问题。如果我们谈论的是ASP.NET MVC,则此设置确保Controller.HttpContextawait之前和之后都包含正确的值。但它并不保证HttpContext.Current包含正确的值,在await之后仍然可能为null


如果我正确理解了您的答案,那么如果在异步EF请求之后有其他服务调用(例如另一个服务)使用其自己的UserService,是否存在问题? - Markus
是的。尝试返回普通对象(避免为EF实体使用延迟加载)。 - Mark Shevchenko
@MarkShevchenko,您能详细说明在正确的web.config设置下,在.NET 4.5.2应用程序中HttpContext.Current可能为空的情况吗?我正在解决一个大型遗留的.NET应用程序中的问题,而您的评论是我看到的第一个暗示在await之后null是有效状态的。 - xxbbcc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接