任务ContinueWith不返回预期值

时间:2014-05-26 21:20:12

标签: c# .net asp.net-web-api task-parallel-library async-await

这可能是那些RTFM类型的问题之一,但是,我不能为我的生活找出链接任务如何以适用于Asp.net WebApi的方式。

具体来说,我正在寻找如何使用DelegatingHandler在控制器处理完响应之后修改响应(添加额外的标题),我试图通过单元测试DelegatingHandler使用HttpMessageInvoker实例。

第一个模式

[TestMethod]
public void FirstTest()
{
    var task = new Task<string>(() => "Foo");

    task.ContinueWith(t => "Bar");

    task.Start();

    Assert.AreEqual("Bar", task.Result);
}

断言失败,因为task.Result返回"Foo"

第二种模式

[TestMethod]
public void SecondTest()
{
    var task = new Task<string>(() => "Foo");

    var continueTask = task.ContinueWith(t => "Bar");

    continueTask.Start();

    Assert.AreEqual("Bar", continueTask.Result);
}

continueTask.Start()上的此操作失败,但 System.InvalidOperationException:可能无法在继续任务上调用Start。

第三种模式

[TestMethod]
public void ThirdTest()
{
    var task = new Task<string>(() => "Foo");

    var continueTask = task.ContinueWith(t => "Bar");

    task.Start();

    Assert.AreEqual("Bar", continueTask.Result);
}

此测试按照我的预期方式工作,但是,我不确定如何使用此模式与WebAPI一起使用。

当前实施

public class BasicAuthenticationHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var task = base.SendAsync(request, cancellationToken);

        task.ContinueWith(AddWwwAuthenticateHeaderTask());

        return task;
    }

    private static Func<Task<HttpResponseMessage>, HttpResponseMessage> 
        AddWwwAuthenticateHeaderTask()
    {
        return task =>
        {
            var response = task.Result;

            if (response.StatusCode == HttpStatusCode.Unauthorized)
            {
                response.Headers.WwwAuthenticate.Add(
                    new AuthenticationHeaderValue("Basic", "realm=\"api\""));
            }

            return response;
        };
    }
}

但是,当我从单元测试中调用BasicAuthenticationHandler时,我的标题在Assert发生之前未添加(如果我调试,我注意到标题是之后添加单元测试失败)。

[TestMethod]
public void should_give_WWWAuthenticate_header_if_authentication_is_missing()
{
    using (var sut = new BasicAuthenticationHandler())
    {
        sut.InnerHandler = new DelegatingHttpMessageHandler(
            () => new HttpResponseMessage(HttpStatusCode.Unauthorized));

        using (var invoker = new HttpMessageInvoker(sut))
        {
            var task = invoker.SendAsync(_requestMessage, CancellationToken.None);

            task.Start();

            Assert.IsTrue(
                task.Result.Headers.WwwAuthenticate.Contains(
                    new AuthenticationHeaderValue("Basic", "realm=\"api\"")));
        }
    }
}

如果我更改生产代码以返回继续任务而不是base.SendAsync的结果,那么我将获得关于在继续任务上调用Start的第二单元测试异常。

我想在我的生产代码中完成第三个单元测试模式,但是,我不知道如何编写它。

我如何做我想做的事情(在断言被调用之前添加标题)?

4 个答案:

答案 0 :(得分:3)

返回Task时,它应始终为Hot Task,表示已返回的任务已启动。让某人明确地在返回的任务上调用Start()会让人感到困惑并违反指南。

要正确查看延续结果,请执行以下操作:

protected override Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request, CancellationToken cancellationToken)
{
    return base.SendAsync(request, cancellationToken).ContinueWith(AddWwwAuthenticateHeaderTask()).Unwrap();
}

您应该修改base.SendAsync以返回已经开始的Task

来自Task Asynchronous Pattern Guidelines

  

从TAP方法返回的所有任务必须“热”。如果TAP方法在内部使用Task的构造函数来实例化要返回的任务,则TAP方法必须在返回Task对象之前调用Start。 TAP方法的消费者可以安全地假设返回的任务是“热门”,并且不应该尝试在从TAP方法返回的任何任务上调用Start。在“热”任务上调用Start将导致InvalidOperationException(此检查由Task类自动处理)。

答案 1 :(得分:3)

尝试以下方法。注意task2.Unwrap(),我认为这部分还没有被其他答案解决:

protected override Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request, CancellationToken cancellationToken)
{
    var task1 = base.SendAsync(request, cancellationToken);

    var task2 = task1.ContinueWith(t => AddWwwAuthenticateHeaderTask(),
        cancellationToken);

    return task2.Unwrap();
}

您需要解开内部任务,因为task2的类型为Task<Task<HttpResponseMessage>>。这应该提供正确的延续语义和结果传播。

检查Stephen Toub的"Processing Sequences of Asynchronous Operations with Tasks"。使用async / await可以避免这种复杂性。

如果您不能使用async/await,您仍然可以进一步改进此代码,以避免由ContinueWith引起的冗余线程切换:

var task2 = task1.ContinueWith(
    t => AddWwwAuthenticateHeaderTask(), 
    cancellationToken,
    TaskContinuationOptions.ExecuteSynchronously,
    TaskScheduler.Default);

在任何一种情况下,继续都不会发生在原始同步上下文(可能是AspNetSynhronizationContext)上。如果您需要保持相同的上下文,请使用TaskScheduler.FromCurrentSynchronizationContext()代替TaskScheduler.Default。请注意:ASP.NET中的this may cause a deadlock

答案 2 :(得分:3)

  

我不能为我的生活弄清楚链接任务是如何以适用于Asp.net WebApi的方式组合在一起的。

拥抱asyncawait。特别是,将ContinueWith替换为await(并且不要使用任务构造函数或Start):

protected override async Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request, CancellationToken cancellationToken)
{
    var response = await base.SendAsync(request, cancellationToken);
    if (response.StatusCode == HttpStatusCode.Unauthorized)
    {
        response.Headers.WwwAuthenticate.Add(
            new AuthenticationHeaderValue("Basic", "realm=\"api\""));
    }

    return response;
}

答案 3 :(得分:1)

ContinueWith返回映射的任务,因此您需要返回:

var task = base.SendAsync(request, cancellationToken);
return task.ContinueWith(AddWwwAuthenticateHeaderTask());