【问题标题】:Task ContinueWith does not return expected value任务 ContinueWith 不返回预期值
【发布时间】:2014-07-15 16:25:55
【问题描述】:

这可能是 RTFM 类型的问题之一,但是,我一生都无法弄清楚如何以适用于 Asp.net WebApi 的方式将任务链接在一起。

具体来说,我正在寻找如何使用DelegatingHandler 来修改控制器处理后的响应(添加额外的标头),并且我正在尝试使用@987654323 对DelegatingHandler 进行单元测试@实例。

第一个模式

[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 may not be called on a continuation task。

第三种模式

[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 的第二个单元测试异常。

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

我该怎么做(在调用断言之前添加标题)?

【问题讨论】:

  • 您使用的是哪个 .NET 框架?
  • 即使任务已经完成,也会继续运行。你假设这不是导致你开始过于谨慎的行为。没必要。

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


【解决方案1】:

试试下面的。注意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&lt;Task&lt;HttpResponseMessage&gt;&gt;。这应该提供正确的延续语义和结果传播。

查看 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】:

    返回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 类自动处理)。

    【讨论】:

    • 感谢您对指南的建议,看来我是在逆向工作。
    • 原来我的问题是我的DelegatingHttpMessageHandler 课程,我没有开始它创建的任务。
    【解决方案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;
    }
    

    【讨论】:

      【解决方案4】:

      ContinueWith 返回映射的任务,所以你需要返回它:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-03-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-03-22
        • 2012-02-04
        • 2012-02-04
        相关资源
        最近更新 更多