Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit 441ffe8

Browse files
committed
Addressed feedback and also added TimeoutCancellationToken feature and also added functional tests now.
1 parent f0cab0f commit 441ffe8

File tree

14 files changed

+323
-74
lines changed

14 files changed

+323
-74
lines changed

src/Microsoft.AspNet.Mvc.Core/Filters/AsyncTimeoutAttribute.cs

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
namespace Microsoft.AspNet.Mvc
1010
{
1111
/// <summary>
12-
/// Represents an attribute that is used to set the timeout value, in milliseconds,
13-
/// which when elapsed will cause the current request to be aborted.
12+
/// Used to set the timeout value, in milliseconds, which when elapsed will
13+
/// cause the current request to be aborted.
1414
/// </summary>
1515
[AttributeUsage(
1616
AttributeTargets.Class | AttributeTargets.Method,
@@ -21,49 +21,37 @@ public class AsyncTimeoutAttribute : Attribute, IAsyncResourceFilter
2121
/// <summary>
2222
/// Initializes a new instance of <see cref="AsyncTimeoutAttribute"/>.
2323
/// </summary>
24-
/// <param name="duration">The duration in milliseconds.</param>
25-
public AsyncTimeoutAttribute(int duration)
24+
/// <param name="durationInMilliseconds">The duration in milliseconds.</param>
25+
public AsyncTimeoutAttribute(int durationInMilliseconds)
2626
{
27-
if (duration < -1)
27+
if (durationInMilliseconds <= 0)
2828
{
29-
throw new ArgumentException(
30-
Resources.FormatAsyncTimeoutAttribute_InvalidTimeout(duration, nameof(duration)));
29+
throw new ArgumentOutOfRangeException(
30+
nameof(durationInMilliseconds),
31+
durationInMilliseconds,
32+
Resources.AsyncTimeoutAttribute_InvalidTimeout);
3133
}
3234

33-
Duration = duration;
35+
DurationInMilliseconds = durationInMilliseconds;
3436
}
3537

3638
/// <summary>
3739
/// The timeout duration in milliseconds.
3840
/// </summary>
39-
public int Duration { get; }
41+
public int DurationInMilliseconds { get; }
4042

4143
public async Task OnResourceExecutionAsync(
4244
[NotNull] ResourceExecutingContext context,
4345
[NotNull] ResourceExecutionDelegate next)
4446
{
45-
var httpContext = context.HttpContext;
46-
47-
// Get a task that will complete after a time delay.
48-
var timeDelayTask = Task.Delay(Duration, cancellationToken: httpContext.RequestAborted);
49-
50-
// Task representing later stages of the pipeline.
51-
var pipelineTask = next();
52-
53-
// Get the first task which completed.
54-
var completedTask = await Task.WhenAny(pipelineTask, timeDelayTask);
55-
56-
if (completedTask == pipelineTask)
57-
{
58-
// Task completed within timeout, but it could be in faulted or canceled state.
59-
// Allow the following line to throw exception and be handled somewhere else.
60-
await completedTask;
61-
}
62-
else
63-
{
64-
// Pipeline task did not complete within timeout, so abort the request.
65-
httpContext.Abort();
66-
}
47+
// Set the feature which provides the cancellation token to later stages
48+
// in the pipeline. This cancellation token gets invoked when the timeout value
49+
// elapses. One can register to this cancellation token to get notified when the
50+
// timeout occurs to take any action.
51+
context.HttpContext.SetFeature<ITimeoutCancellationTokenFeature>(
52+
new TimeoutCancellationTokenFeature(DurationInMilliseconds));
53+
54+
await next();
6755
}
6856
}
6957
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading;
5+
6+
namespace Microsoft.AspNet.Mvc
7+
{
8+
/// <summary>
9+
/// Provides a <see cref="CancellationToken"/> which gets invoked after a specified timeout value.
10+
/// </summary>
11+
public interface ITimeoutCancellationTokenFeature
12+
{
13+
/// <summary>
14+
/// Gets a <see cref="CancellationToken"/> which gets invoked
15+
/// after a specified timeout value.
16+
/// </summary>
17+
CancellationToken TimeoutCancellationToken { get; }
18+
}
19+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace Microsoft.AspNet.Mvc.ModelBinding
8+
{
9+
/// <summary>
10+
/// Represents a model binder which can bind models of type <see cref="CancellationToken"/>.
11+
/// </summary>
12+
/// <remarks>
13+
/// The <see cref="CancellationToken"/> provided by this binder is invoked after a supplied timeout value.
14+
/// This binder depends on the <see cref="ITimeoutCancellationTokenFeature"/> to get the token. This feature
15+
/// is typically set through the <see cref="AsyncTimeoutAttribute"/>.
16+
/// </remarks>
17+
public class TimeoutCancellationTokenModelBinder : IModelBinder
18+
{
19+
/// <inheritdoc />
20+
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
21+
{
22+
if (bindingContext.ModelType == typeof(CancellationToken))
23+
{
24+
var httpContext = bindingContext.OperationBindingContext.HttpContext;
25+
26+
var timeoutTokenFeature = httpContext.GetFeature<ITimeoutCancellationTokenFeature>();
27+
if (timeoutTokenFeature != null)
28+
{
29+
return Task.FromResult(new ModelBindingResult(
30+
model: timeoutTokenFeature.TimeoutCancellationToken,
31+
key: bindingContext.ModelName,
32+
isModelSet: true));
33+
}
34+
else
35+
{
36+
return Task.FromResult(new ModelBindingResult(
37+
model: CancellationToken.None,
38+
key: bindingContext.ModelName,
39+
isModelSet: false));
40+
}
41+
}
42+
43+
return Task.FromResult<ModelBindingResult>(null);
44+
}
45+
}
46+
}

src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.AspNet.Mvc.Core/Resources.resx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,6 @@
449449
<value>The model's runtime type '{0}' is not assignable to the type '{1}'.</value>
450450
</data>
451451
<data name="AsyncTimeoutAttribute_InvalidTimeout" xml:space="preserve">
452-
<value>The value '{0}' for argument '{1}' is invalid. The timeout value must be non-negative.</value>
452+
<value>The timeout value must be greater than 0.</value>
453453
</data>
454454
</root>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading;
6+
using Microsoft.AspNet.Mvc.Core;
7+
8+
namespace Microsoft.AspNet.Mvc
9+
{
10+
/// <inheritdoc />
11+
public class TimeoutCancellationTokenFeature : ITimeoutCancellationTokenFeature
12+
{
13+
/// <summary>
14+
/// Initializes a new instance of <see cref="TimeoutCancellationTokenFeature"/>.
15+
/// </summary>
16+
/// <param name="durationInMilliseconds">The duration in milliseconds.</param>
17+
public TimeoutCancellationTokenFeature(int durationInMilliseconds)
18+
{
19+
if (durationInMilliseconds <= 0)
20+
{
21+
throw new ArgumentOutOfRangeException(
22+
nameof(durationInMilliseconds),
23+
durationInMilliseconds,
24+
Resources.AsyncTimeoutAttribute_InvalidTimeout);
25+
}
26+
27+
var timeoutCancellationTokenSource = new CancellationTokenSource();
28+
timeoutCancellationTokenSource.CancelAfter(millisecondsDelay: durationInMilliseconds);
29+
30+
TimeoutCancellationToken = timeoutCancellationTokenSource.Token;
31+
}
32+
33+
/// <inheritdoc />
34+
public CancellationToken TimeoutCancellationToken { get; }
35+
}
36+
}

src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public static void ConfigureMvcOptions(MvcOptions options)
3434
options.ModelBinders.Add(new HeaderModelBinder());
3535
options.ModelBinders.Add(new TypeConverterModelBinder());
3636
options.ModelBinders.Add(new TypeMatchModelBinder());
37-
options.ModelBinders.Add(new CancellationTokenModelBinder());
37+
options.ModelBinders.Add(new TimeoutCancellationTokenModelBinder());
3838
options.ModelBinders.Add(new ByteArrayModelBinder());
3939
options.ModelBinders.Add(new FormFileModelBinder());
4040
options.ModelBinders.Add(new FormCollectionModelBinder());

test/Microsoft.AspNet.Mvc.Core.Test/Filters/AsyncTimeoutAttributeTest.cs

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#if ASPNET50
55
using System.Threading.Tasks;
66
using Microsoft.AspNet.Http;
7+
using Microsoft.AspNet.Http.Core;
78
using Microsoft.AspNet.Routing;
89
using Moq;
910
using Xunit;
@@ -13,47 +14,22 @@ namespace Microsoft.AspNet.Mvc.Test
1314
public class AsyncTimeoutAttributeTest
1415
{
1516
[Fact]
16-
public async Task RequestIsAborted_AfterTimeoutDurationElapses()
17+
public async Task SetsTimeoutCancellationTokenFeature_OnExecution()
1718
{
1819
// Arrange
19-
var httpContext = new Mock<HttpContext>();
20+
var httpContext = new DefaultHttpContext();
2021
var asyncTimeoutAttribute = new AsyncTimeoutAttribute(1 * 1000); // 1 second
21-
var resourceExecutingContext = GetResourceExecutingContext(httpContext.Object);
22-
23-
// Act
24-
await asyncTimeoutAttribute.OnResourceExecutionAsync(
25-
resourceExecutingContext,
26-
async () =>
27-
{
28-
// Imagine here the rest of pipeline(ex: model-binding->action filters-action) being executed
29-
await Task.Delay(10 * 1000); // 10 seconds
30-
return null;
31-
});
32-
33-
// Assert
34-
httpContext.Verify(hc => hc.Abort(), Times.Once);
35-
}
22+
var resourceExecutingContext = GetResourceExecutingContext(httpContext);
3623

37-
[Fact]
38-
public async Task RequestIsNotAborted_BeforeTimeoutDurationElapses()
39-
{
40-
// Arrange
41-
var httpContext = new Mock<HttpContext>();
42-
var asyncTimeoutAttribute = new AsyncTimeoutAttribute(10 * 1000); // 10 seconds
43-
var resourceExecutingContext = GetResourceExecutingContext(httpContext.Object);
44-
4524
// Act
4625
await asyncTimeoutAttribute.OnResourceExecutionAsync(
4726
resourceExecutingContext,
48-
async () =>
49-
{
50-
// Imagine here the rest of pipeline(ex: model-binding->action filters-action) being executed
51-
await Task.Delay(1 * 1000); // 1 second
52-
return null;
53-
});
27+
() => Task.FromResult<ResourceExecutedContext>(null));
5428

5529
// Assert
56-
httpContext.Verify(hc => hc.Abort(), Times.Never);
30+
var timeoutFeature = resourceExecutingContext.HttpContext.GetFeature<ITimeoutCancellationTokenFeature>();
31+
Assert.NotNull(timeoutFeature);
32+
Assert.NotNull(timeoutFeature.TimeoutCancellationToken);
5733
}
5834

5935
private ResourceExecutingContext GetResourceExecutingContext(HttpContext httpContext)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Net;
7+
using System.Threading.Tasks;
8+
using BasicWebSite;
9+
using Microsoft.AspNet.Builder;
10+
using Microsoft.AspNet.TestHost;
11+
using Newtonsoft.Json;
12+
using Xunit;
13+
14+
namespace Microsoft.AspNet.Mvc.FunctionalTests
15+
{
16+
public class AsyncTimeoutAttributeTest
17+
{
18+
private readonly IServiceProvider _provider = TestHelper.CreateServices(nameof(BasicWebSite));
19+
private readonly Action<IApplicationBuilder> _app = new Startup().Configure;
20+
21+
[Theory]
22+
[InlineData("http://localhost/AsyncTimeout/ActionWithTimeoutAttribute")]
23+
[InlineData("http://localhost/AsyncTimeoutOnController/ActionWithTimeoutAttribute")]
24+
public async Task AsyncTimeOutAttribute_IsDecorated_AndCancellationTokenIsBound(string url)
25+
{
26+
// Arrange
27+
var server = TestServer.Create(_provider, _app);
28+
var client = server.CreateClient();
29+
var expected = "CancellationToken is present";
30+
31+
// Act
32+
var response = await client.GetAsync(url);
33+
34+
// Assert
35+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
36+
var data = await response.Content.ReadAsStringAsync();
37+
Assert.Equal(expected, data);
38+
}
39+
40+
[Fact]
41+
public async Task AsyncTimeOutAttribute_IsNotDecorated_AndCancellationToken_IsNone()
42+
{
43+
// Arrange
44+
var server = TestServer.Create(_provider, _app);
45+
var client = server.CreateClient();
46+
var expected = "CancellationToken is not present";
47+
48+
// Act
49+
var response = await client.GetAsync("http://localhost/AsyncTimeout/ActionWithNoTimeoutAttribute");
50+
51+
// Assert
52+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
53+
var data = await response.Content.ReadAsStringAsync();
54+
Assert.Equal(expected, data);
55+
}
56+
57+
[Theory]
58+
[InlineData("http://localhost/AsyncTimeout")]
59+
[InlineData("http://localhost/AsyncTimeoutOnController")]
60+
public async Task TimeoutIsTriggered(string baseUrl)
61+
{
62+
// Arrange
63+
var expected = "Hello World!";
64+
var expectedCorrelationId = Guid.NewGuid().ToString();
65+
var server = TestServer.Create(_provider, _app);
66+
var client = server.CreateClient();
67+
client.DefaultRequestHeaders.Add("CorrelationId", expectedCorrelationId);
68+
69+
// Act
70+
var response = await client.GetAsync(string.Format("{0}/LongRunningAction", baseUrl));
71+
72+
// Assert
73+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
74+
var data = await response.Content.ReadAsStringAsync();
75+
Assert.Equal(expected, data);
76+
77+
response = await client.GetAsync(string.Format("{0}/TimeoutTriggerLogs", baseUrl));
78+
data = await response.Content.ReadAsStringAsync();
79+
var timeoutTriggerLogs = JsonConvert.DeserializeObject<List<string>>(data);
80+
Assert.NotNull(timeoutTriggerLogs);
81+
Assert.Contains(expectedCorrelationId, timeoutTriggerLogs);
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)