При использовании ASP.NET Web API мой ExecutionContext не передается в асинхронных действиях

У меня возникают трудности с пониманием механики позади ExecutionContext.

Из того, что я читал в Интернете, контекстно-зависимые элементы, такие как безопасность (Thread Principal), культура и т. Д., Должны проходить через асинхронные потоки в пределах единицы выполнения работы.

Я сталкиваюсь с очень запутанными и потенциально опасными ошибками. Я заметил, что CurrentPrincipal моего потока теряется при асинхронном выполнении.


Вот пример сценария ASP.NET Web API:

Во-первых, давайте настроим простую конфигурацию Web API с двумя делегирующими обработчиками для целей тестирования.

Все, что они делают, это записывают отладочную информацию и пропускают запрос / ответ, за исключением первого "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;
    }
}

Достаточно просто. Далее давайте добавим один ApiController для обработки HTTP POST, как если бы вы загружали файлы.

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

Чтобы сделать вещи более запутанными, когда я добавляю следующее в асинхронную строку:

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

Дело здесь в этом. Код, следующий за асинхронным, на самом деле вызывает мою бизнес-логику или просто требует правильной установки контекста безопасности. Существует потенциальная проблема целостности происходит.

Может ли кто-нибудь помочь пролить свет на то, что происходит?

Заранее спасибо.

1 ответ

Решение

У меня нет ответов на все вопросы, но я могу помочь заполнить некоторые пробелы и угадать проблему.

По умолчанию ASP.NET SynchronizationContext будет течь, но то, как он течет, идентичность немного странная. Это на самом деле течет HttpContext.Current.User а затем устанавливает Thread.CurrentPrincipal к этому. Так что если вы просто установите Thread.CurrentPrincipal, вы не увидите, как он течет правильно.

На самом деле вы увидите следующее поведение:

  • От времени Thread.CurrentPrincipal настроен на поток, этот поток будет иметь тот же принципал, пока он не войдет в контекст ASP.NET.
  • Когда какой-либо поток входит в контекст ASP.NET, Thread.CurrentPrincipal очищается (потому что установлено HttpContext.Current.User).
  • Когда поток используется вне контекста ASP.NET, он просто сохраняет все Thread.CurrentPrincipal случилось быть установленным на этом.

Применяя это к вашему исходному коду и выводу:

  • Первые 3 все сообщаются синхронно из потока 63 после его CurrentPrincipal был явно установлен, поэтому все они имеют ожидаемое значение.
  • Тема 77 используется для возобновления async метод, таким образом, ввод контекста ASP.NET и очистки любого CurrentPrincipal это могло иметь.
  • Тема 63 используется для ProcessResponse, Он снова входит в контекст ASP.NET, очищая его Thread.CurrentPrincipal,
  • Тема 65 интересная. Он работает вне контекста ASP.NET (в ContinueWith без планировщика), поэтому он просто сохраняет все CurrentPrincipal это случилось раньше. Я предполагаю, что его CurrentPrincipal только что перенесен из более раннего теста.

Обновленный код меняется PostFile запустить свою вторую часть вне контекста ASP.NET. Таким образом, он получает поток 65, который просто имеет CurrentPrincipal задавать. Поскольку это вне контекста ASP.NET, CurrentPrincipal не очищается

Итак, это выглядит как ExecutionContext течет нормально. Я уверен, что Microsoft проверила ExecutionContext вытекать из вазу; в противном случае каждое приложение ASP.NET в мире имело бы серьезный недостаток безопасности. Важно отметить, что в этом коде Thread.CurrentPrincipal просто относится к претензиям текущего пользователя и не представляет фактическое олицетворение.

Если мои догадки верны, то исправить это довольно просто: в SendAsyncизмените эту строку:

Thread.CurrentPrincipal = new ClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new[]{ new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "dgdev") }, "myauthisthebest")));

к этому:

HttpContext.Current.User = new ClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new[]{ new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "dgdev") }, "myauthisthebest")));
Thread.CurrentPrincipal = HttpContext.Current.User;

Я понимаю, что повторный вход в контекст синхронизации ASP.NET приведет к тому, что для Thread.CurrentPrincipal будет установлено значение HttpContext.Current.User. Но я все еще не вижу поведение, которое я ожидал. Я не ожидал, что при каждом ожидаемом вызове цепочки будет установлен Thread.CurrentPrincipal = HttpContext.Current.User. Я вижу, что это даже выходит за пределы обработчика событий async void, в котором я запустил цепочку async/await. Это поведение других людей? Я ожидал, что вызовы в цепочке будут использовать их захваченный контекст для продолжения, но они демонстрируют повторное поступление.

Я не использую.ContinueAwait(false) ни в одном из ожидаемых звонков. У нас есть targetFramework="4.6.1" в файле web.config, который, помимо прочего, устанавливает UseTaskFriendlySynchronizationContext = true, среди прочего. Сторонний API-клиент вызывает реентерабельное поведение в нижней части цепочки async/await.

Другие вопросы по тегам