问题 DelegatingHandler应该如何进行异步调用(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绑定操作。 IO绑定操作已经被包装为a Task<bool>。我需要使用结果来确定是否继续内部处理程序。

一个例子是进行网络调用以授权请求。我必须这样做才能与现有系统集成。一般来说,我认为这个问题有一些有效的方案,它应该有一个可行的解决方案。

在这种情况下,实现SendAsync的正确方法是什么,以便我异步执行IO绑定任务,然后继续异步执行内部处理程序?

关键是我想确定请求线程在任何时候都不会被阻塞。


6181
2018-06-14 14:48


起源



答案:


好的,我想我已经破了。我用身份验证方案说明了这一点:我想异步验证用户并使用结果来决定是返回401还是继续使用消息处理程序链。

核心问题是,在获得异步身份验证的结果之前,不能调用内部处理程序SendAsync()。

对我来说,关键的见解是使用TaskCompletionSource(TCS)来控制执行流程。这使我能够从TCS返回任务并在我喜欢时设置结果 - 最重要的是延迟调用SendAsync()直到我知道我需要它。

因此,我设置了TCS,然后开始执行授权任务。在此继续,我看看结果。如果被授权,我调用内部处理程序链并附加一个延续 (避免任何线程阻塞) 完成TCS。如果验证失败,我只需在那里完成TCS,然后使用401。

结果是两个异步任务轮流执行而没有任何线程阻塞。我加载测试这个似乎工作得很好。

尽管使用async / await语法在.NET 4.5中更好一点......虽然TCS的方法基本上仍然在幕后发生,但代码要简单得多。

请享用!

第一个片段是在.NET 4.0上使用Web API Beta构建的 - 这是.NET 4.5 / Web 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;
}

这是一个.NET 4.5 / Web API Release Candidate版本,它与新的async / await语法相比更加性感:

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);
}

15
2018-06-18 10:13



你是怎么实施的 Authorize?你有没有实现新的 HttpClient? - JobaDiniz
Authorize的实现与此无关 - 它只是一个异步函数,用于确定是否允许请求 - 您如何实现它取决于您。例如,可以进行数据库检查以查看当前用户是否有权根据某些定制业务规则发出当前请求。 - James World
我只是想知道你是否发出了另一个http请求,这就是...在我的场景中我需要在当前的一个之前再进行一次http调用,然后我又实现另一个http调用 HttpClient。我想知道我是否可以在处理程序中重用当前的那个,但似乎我不能 - JobaDiniz
一个新的独立实例是个好主意,是的。 - James World


答案:


好的,我想我已经破了。我用身份验证方案说明了这一点:我想异步验证用户并使用结果来决定是返回401还是继续使用消息处理程序链。

核心问题是,在获得异步身份验证的结果之前,不能调用内部处理程序SendAsync()。

对我来说,关键的见解是使用TaskCompletionSource(TCS)来控制执行流程。这使我能够从TCS返回任务并在我喜欢时设置结果 - 最重要的是延迟调用SendAsync()直到我知道我需要它。

因此,我设置了TCS,然后开始执行授权任务。在此继续,我看看结果。如果被授权,我调用内部处理程序链并附加一个延续 (避免任何线程阻塞) 完成TCS。如果验证失败,我只需在那里完成TCS,然后使用401。

结果是两个异步任务轮流执行而没有任何线程阻塞。我加载测试这个似乎工作得很好。

尽管使用async / await语法在.NET 4.5中更好一点......虽然TCS的方法基本上仍然在幕后发生,但代码要简单得多。

请享用!

第一个片段是在.NET 4.0上使用Web API Beta构建的 - 这是.NET 4.5 / Web 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;
}

这是一个.NET 4.5 / Web API Release Candidate版本,它与新的async / await语法相比更加性感:

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);
}

15
2018-06-18 10:13



你是怎么实施的 Authorize?你有没有实现新的 HttpClient? - JobaDiniz
Authorize的实现与此无关 - 它只是一个异步函数,用于确定是否允许请求 - 您如何实现它取决于您。例如,可以进行数据库检查以查看当前用户是否有权根据某些定制业务规则发出当前请求。 - James World
我只是想知道你是否发出了另一个http请求,这就是...在我的场景中我需要在当前的一个之前再进行一次http调用,然后我又实现另一个http调用 HttpClient。我想知道我是否可以在处理程序中重用当前的那个,但似乎我不能 - JobaDiniz
一个新的独立实例是个好主意,是的。 - James World