web-dev-qa-db-ja.com

非同期待機で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 私のコードはまだ動作します。これを処理するためのより良いアプローチはありますか?サービスへのすべてのリクエストでユーザーに提供したくありません。

32
Magu

_web.config_設定が正しい である限り、async/awaitは_HttpContext.Current_と完全に連携します。 httpRuntimetargetFrameworkを_4.5_に設定して、すべての「互換モード」動作を削除することをお勧めします。

それが完了すると、普通のasync/awaitは完全に機能します。別のスレッドで作業を行っている場合、またはawaitコードが正しくない場合にのみ問題が発生します。


まず、「他のスレッド」の問題。これは、リンク先のブログ投稿の2番目の問題です。もちろん、このようなコードは正しく機能しません。

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

この問題は、実際には非同期コードとは関係ありません。 (非要求)スレッドプールスレッドからコンテキスト変数を取得する必要があります。同期して実行しようとすると、まったく同じ問題が発生します。

コアの問題は、非同期バージョンがfake非同期を使用していることです。これは、特にASP.NETでは不適切です。解決策は、単純に偽の非同期コードを削除し、同期(または実際に非同期の作業を実際に行う場合は真に非同期)にすることです。

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

リンクされたブログで推奨されている手法(HttpContextをラップしてワーカースレッドに提供する)は非常に危険です。 HttpContextは、一度に1つのスレッドからのみアクセスされるように設計されており、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;
}
_

コンテキストはインターフェイスの背後に隠されているため、このコードは同じくらい間違っていますが、明らかに間違っているわけではありません。

したがって、一般的なガイドラインは次のとおりです。use ConfigureAwait(false) if ifknowメソッドが依存しないことそのコンテキスト(直接または間接);それ以外の場合は、ConfigureAwaitを使用しないでください。設計でインターフェイス実装がその実装のコンテキストを使用することが許容される場合、インターフェイスメソッドを呼び出すメソッドはすべて、notConfigureAwait(false)を使用する必要があります。

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

このガイドラインに従う限り、async/awaitは_HttpContext.Current_と完全に連携します。

54
Stephen Cleary

非同期は問題ありません。問題は、作業を別のスレッドに投稿するときです。アプリケーションが4.5+として設定されている場合、非同期コールバックは元のコンテキストでポストされるため、適切なHttpContextなども使用できます。

とにかく別のスレッドで共有状態にアクセスしたくないので、Tasksを使用して明示的に処理する必要はほとんどありません。すべての入力を引数として入力し、応答のみを返すようにしてください。共有状態への読み取りまたは書き込みよりも(例:HttpContext、静的フィールドなど)

2
Luaan

あなたのViewModels.DisplayChannelは、追加のロジックのない単純なオブジェクトです。

Taskの結果が「いくつかのコンテキストオブジェクト」を参照している場合、問題が発生する可能性があります。 HttpContext.Current。このようなオブジェクトは多くの場合スレッドにアタッチされますが、awaitの後のコード全体が別のスレッドで実行される場合があります。

UseTaskFriendlySynchronizationContextはすべての問題を解決するわけではないことに注意してください。 ASP.NET MVCについて話している場合、この設定によりController.HttpContextには、前と同様に正しい値が含まれますawaitは後と同様です。しかし、それはHttpContext.Currentには正しい値が含まれており、awaitの後でもnullにできます。

2
Mark Shevchenko