22

ExecutionContext の背後にあるメカニズムを理解するのに苦労しています。

私がオンラインで読んだことから、セキュリティ (スレッド プリンシパル)、カルチャなどのコンテキストに依存する項目は、実行単位の作業の範囲内で非同期スレッド間を流れる必要があります。

ただし、非常に紛らわしく、潜在的に危険なバグに遭遇しています。スレッドの CurrentPrincipal が非同期実行中に失われていることに気付きました。


ASP.NET Web API シナリオの例を次に示します。

最初に、テスト用に 2 つの委任ハンドラーを使用して単純な Web API 構成をセットアップしましょう。

スレッドのプリンシパルと、コンテキスト全体で共有されるデータの一部 (要求の相関 ID) を設定する最初の「DummyHandler」を除いて、デバッグ情報を書き出して要求/応答を渡すだけです。

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MessageHandlers.Add(new DummyHandler());
        config.MessageHandlers.Add(new AnotherDummyHandler());

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

public class DummyHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        CallContext.LogicalSetData("rcid", request.GetCorrelationId());
        Thread.CurrentPrincipal = new ClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new[]{ new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "dgdev") }, "myauthisthebest")));

        Debug.WriteLine("Dummy Handler Thread: {0}", Thread.CurrentThread.ManagedThreadId);
        Debug.WriteLine("User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name);
        Debug.WriteLine("RCID: {0}", CallContext.LogicalGetData("rcid"));

        return base.SendAsync(request, cancellationToken)
                   .ContinueWith(task =>
                       {
                           Debug.WriteLine("Dummy Handler Thread: {0}", Thread.CurrentThread.ManagedThreadId);
                           Debug.WriteLine("User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name);
                           Debug.WriteLine("RCID: {0}", CallContext.LogicalGetData("rcid"));

                           return task.Result;
                       });
    }
}

public class AnotherDummyHandler : MessageProcessingHandler
{
    protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Debug.WriteLine("  Another Dummy Handler Thread: {0}", Thread.CurrentThread.ManagedThreadId);
        Debug.WriteLine("  User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name);
        Debug.WriteLine("  RCID: {0}", CallContext.LogicalGetData("rcid"));

        return request;
    }

    protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken)
    {
        Debug.WriteLine("  Another Dummy Handler Thread: {0}", Thread.CurrentThread.ManagedThreadId);
        Debug.WriteLine("  User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name);
        Debug.WriteLine("  RCID: {0}", CallContext.LogicalGetData("rcid"));

        return response;
    }
}

十分に単純です。次に、ファイルをアップロードするかのように、HTTP POST を処理する単一の ApiController を追加しましょう。

public class UploadController : ApiController
{
    public async Task<HttpResponseMessage> PostFile()
    {
        Debug.WriteLine("    Thread: {0}", Thread.CurrentThread.ManagedThreadId);
        Debug.WriteLine("    User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name);
        Debug.WriteLine("    RCID: {0}", CallContext.LogicalGetData("rcid"));

        if (!Request.Content.IsMimeMultipartContent())
        {
            throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
        }

        try
        {
            await Request.Content.ReadAsMultipartAsync(
                new MultipartFormDataStreamProvider(
                    HttpRuntime.AppDomainAppPath + @"upload\temp"));

            Debug.WriteLine("    Thread: {0}", Thread.CurrentThread.ManagedThreadId);
            Debug.WriteLine("    User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name);
            Debug.WriteLine("    RCID: {0}", CallContext.LogicalGetData("rcid"));

            return new HttpResponseMessage(HttpStatusCode.Created);
        }
        catch (Exception e)
        {
            return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e);
        }
    }
}

Fiddler でテストを実行すると、次のような出力が表示されます。

Dummy Handler Thread: 63
User: dgdev
RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476

  Another Dummy Handler Thread: 63
  User: dgdev
  RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476

    Thread: 63
    User: dgdev
    RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476

    Thread: 77
    User:                                     <<<  PRINCIPAL IS LOST AFTER ASYNC
    RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476

  Another Dummy Handler Thread: 63
  User:                                       <<<  PRINCIPAL IS STILL LOST
  RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476

Dummy Handler Thread: 65
User: dgdev                                   <<<  PRINCIPAL IS BACK?!?
RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476

さらに混乱させるために、 async 行に次の行を追加すると、次のようになります。

await Request.Content.ReadAsMultipartAsync(
    new MultipartFormDataStreamProvider(..same as before..))
.ConfigureAwait(false); <<<<<<

私は今、この出力を受け取ります:

Dummy Handler Thread: 40
User: dgdev
RCID: 8d944500-cb52-4362-8537-dab405fa12a2

  Another Dummy Handler Thread: 40
  User: dgdev
  RCID: 8d944500-cb52-4362-8537-dab405fa12a2

    Thread: 40
    User: dgdev
    RCID: 8d944500-cb52-4362-8537-dab405fa12a2

    Thread: 65
    User: dgdev                               <<<  PRINCIPAL IS HERE!
    RCID: 8d944500-cb52-4362-8537-dab405fa12a2

  Another Dummy Handler Thread: 65
  User:                                       <<<  PRINCIPAL IS LOST
  RCID: 8d944500-cb52-4362-8537-dab405fa12a2

Dummy Handler Thread: 40
User: dgdev
RCID: 8d944500-cb52-4362-8537-dab405fa12a2

ここでのポイントはこれです。async my に続くコードは、実際にはビジネス ロジックを呼び出すか、単にセキュリティ コンテキストを適切に設定する必要があります。整合性の問題が発生する可能性があります。

何が起こっているのかを明らかにするのを手伝ってくれる人はいますか?

前もって感謝します。

4

2 に答える 2