Как должен делегироватьHandler делать асинхронный вызов (ASP.NET MVC Web API)?

Мне удобно выполнять синхроническую работу перед вызовом внутреннего обработчика SendAsync() и выполнять синхронную работу после завершения внутреннего обработчика с помощью завершения. например:

protected override Task<HttpResponseMessage> SendAsync( 
    HttpRequestMessage request, CancellationToken cancellationToken) 
{ 
    // do some sync work before inner handler here 

    var sendTask = base.SendAsync(request, cancellationToken); 
    return sendTask.ContinueWith( 
        task => { // do some sync work afterwards here }); 
} 

Однако теперь мне нужно вызвать операцию привязки ввода-вывода из обработчика делегирования. Операция привязки IO уже завершена как Task<bool>. Мне нужно использовать результат, чтобы определить, продолжать ли внутренний обработчик.

Примером может быть сетевой вызов для авторизации запроса. Я должен сделать это, чтобы интегрироваться с существующей системой. В общем, я думаю, что для этой проблемы существуют допустимые сценарии, и она должна иметь работоспособное решение.

Каков правильный способ реализации SendAsync в этом случае, так что я выполняю асинхронно связанную с IO задачу и продолжаю асинхронно выполнять внутренний обработчик?

Ключевым моментом является то, что я хочу быть уверенным, что поток запроса не будет заблокирован в любой момент.

Ответ 1

Хорошо, я думаю, что у меня это треснуло. Я иллюстрирую это сценарием аутентификации: я хочу асинхронно аутентифицировать пользователя и использовать результат, чтобы решить, следует ли возвращать 401 или продолжить цепочку обработчиков сообщений.

Центральная проблема заключается в том, что вы не можете вызвать внутренний обработчик SendAsync(), пока не получите результат от асинхронной аутентификации.

Ключевым понятием для меня было использование TaskCompletionSource (TCS) для управления потоком выполнения. Это позволило мне вернуть задачу из TCS и установить результат на нее всякий раз, когда мне понравилось - и, самое главное, отложить вызов SendAsync(), пока я не знаю, что мне это нужно.

Итак, я создал TCS, а затем запустил задачу для авторизации. В продолжение этого я смотрю на результат. Если разрешено, я вызываю внутреннюю цепочку обработчиков и присоединяю продолжение к этому (избегая блокировки потока), который завершает TCS. Если проверка подлинности не удалась, я просто завершу TCS там, а затем с 401.

Результатом этого является то, что обе асинхронные задачи выполняются по очереди без блокировки потоков. Я загружаю протестированные данные, и кажется, что он работает нормально.

В .NET 4.5 все гораздо приятнее с синтаксисом async/await, хотя... хотя подход с TCS по-прежнему в основном происходит под обложками, код намного проще.

Наслаждайтесь!

Первый фрагмент был создан на .NET 4.0 с бета-версией веб-API - второй на .NET 4.5/веб-API RC.

protected override Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request, CancellationToken cancellationToken)
{
    var taskCompletionSource = new TaskCompletionSource<HttpResponseMessage>();

    // Authorize() returns a started
    // task that authenticates the user
    // if the result is false we should
    // return a 401 immediately
    // otherwise we can invoke the inner handler
    Task<bool> authenticationTask = Authorize(request);

    // attach a continuation...
    authenticationTask.ContinueWith(_ =>
    {
        if (authenticationTask.Result)
        {
            // authentication succeeded
            // so start the inner handler chain
            // and write the result to the
            // task completion source when done
            base.SendAsync(request, cancellationToken)
                .ContinueWith(t => taskCompletionSource.SetResult(t.Result));
        }
        else
        {
            // authentication failed
            // so complete the TCS immediately
            taskCompletionSource.SetResult(
                new HttpResponseMessage(HttpStatusCode.Unauthorized));
        }
    });

    return taskCompletionSource.Task;
}

Вот версия кандидата 4.5/Web API Release Candidate, которая намного сексуальнее с новым синтаксисом async/wait:

protected override async Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request, CancellationToken cancellationToken)
{
    // Authorize still has a Task<bool> return type
    // but await allows this nicer inline syntax
    var authorized = await Authorize(request);

    if (!authorized)
    {
        return new HttpResponseMessage(HttpStatusCode.Unauthorized)
        {
            Content = new StringContent("Unauthorized.")
        };
    }

    return await base.SendAsync(request, cancellationToken);
}