这可能是那些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
的第二单元测试异常。
我想我想在我的生产代码中完成第三个单元测试模式,但是,我不知道如何编写它。
我如何做我想做的事情(在断言被调用之前添加标题)?
答案 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的方式组合在一起的。
拥抱async
和await
。特别是,将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());