Skip to content

Commit 4820a0c

Browse files
Merge pull request #7 from candoumbe/feature/add-cancellation-token-support
feat(hosting) : add cancellation token support
2 parents 4f53279 + 1325638 commit 4820a0c

File tree

8 files changed

+105
-32
lines changed

8 files changed

+105
-32
lines changed

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ A simple helper to perform async application initialization for the generic host
3535
...
3636
}
3737
38-
public async Task InitializeAsync()
38+
public async Task InitializeAsync(CancellationToken cancellationToken)
3939
{
4040
// Initialization code here
4141
}
@@ -59,6 +59,25 @@ A simple helper to perform async application initialization for the generic host
5959
}
6060
```
6161
62+
You can also pass a `CancellationToken` in order to propagate notifications to cancel the initialization if needed.
63+
64+
In the following example, the initialization will be cancelled when `Ctrl + C` keys are pressed :
65+
```csharp
66+
public static async Task Main(string[] args)
67+
{
68+
using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
69+
70+
// The following line will hook `Ctrl` + `C` to the cancellation token.
71+
Console.CancelKeyPress += (source, args) => cancellationTokenSource.Cancel();
72+
73+
var host = CreateHostBuilder(args).Build();
74+
75+
await host.InitAsync(cancellationTokenSource.Token);
76+
await host.RunAsync();
77+
}
78+
```
79+
80+
6281
(Note that you need to [set the C# language version to 7.1 or higher in your project](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/configure-language-version#edit-the-csproj-file) to enable the "async Main" feature.)
6382
6483
This will run each initializer, in the order in which they were registered.

src/Extensions.Hosting.AsyncInitialization/DependencyInjection/AsyncInitializationServiceCollectionExtensions.cs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Threading;
23
using System.Threading.Tasks;
34
using Extensions.Hosting.AsyncInitialization;
45
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -96,6 +97,22 @@ public static IServiceCollection AddAsyncInitializer(this IServiceCollection ser
9697
/// <param name="initializer">The delegate that performs async initialization.</param>
9798
/// <returns>A reference to this instance after the operation has completed.</returns>
9899
public static IServiceCollection AddAsyncInitializer(this IServiceCollection services, Func<Task> initializer)
100+
{
101+
if (initializer == null)
102+
throw new ArgumentNullException(nameof(initializer));
103+
104+
return services
105+
.AddAsyncInitialization()
106+
.AddSingleton<IAsyncInitializer>(new DelegateAsyncInitializer(_ => initializer()));
107+
}
108+
109+
/// <summary>
110+
/// Adds an async initializer whose implementation is the specified delegate.
111+
/// </summary>
112+
/// <param name="services">The <see cref="T:Microsoft.Extensions.DependencyInjection.IServiceCollection" /> to add the service to.</param>
113+
/// <param name="initializer">The delegate that performs async initialization.</param>
114+
/// <returns>A reference to this instance after the operation has completed.</returns>
115+
public static IServiceCollection AddAsyncInitializer(this IServiceCollection services, Func<CancellationToken, Task> initializer)
99116
{
100117
if (initializer == null)
101118
throw new ArgumentNullException(nameof(initializer));
@@ -107,16 +124,16 @@ public static IServiceCollection AddAsyncInitializer(this IServiceCollection ser
107124

108125
private class DelegateAsyncInitializer : IAsyncInitializer
109126
{
110-
private readonly Func<Task> _initializer;
127+
private readonly Func<CancellationToken, Task> _initializer;
111128

112-
public DelegateAsyncInitializer(Func<Task> initializer)
129+
public DelegateAsyncInitializer(Func<CancellationToken, Task> initializer)
113130
{
114131
_initializer = initializer;
115132
}
116133

117-
public Task InitializeAsync()
134+
public Task InitializeAsync(CancellationToken cancellationToken)
118135
{
119-
return _initializer();
136+
return _initializer(cancellationToken);
120137
}
121138
}
122139
}

src/Extensions.Hosting.AsyncInitialization/Extensions.Hosting.AsyncInitialization.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<TargetFramework>netstandard2.0</TargetFramework>
5-
<LangVersion>8.0</LangVersion>
5+
<LangVersion>latest</LangVersion>
66
<Nullable>enable</Nullable>
77
<Authors>Thomas Levesque</Authors>
88
<Title>.NET Core generic host async initialization</Title>
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Threading;
23
using System.Threading.Tasks;
34
using Extensions.Hosting.AsyncInitialization;
45
using Microsoft.Extensions.DependencyInjection;
@@ -15,22 +16,21 @@ public static class AsyncInitializationHostExtensions
1516
/// Initializes the application, by calling all registered async initializers.
1617
/// </summary>
1718
/// <param name="host">The host.</param>
19+
/// <param name="cancellationToken">Optionally propagates notifications that the operation should be cancelled</param>
1820
/// <returns>A task that represents the initialization completion.</returns>
19-
public static async Task InitAsync(this IHost host)
21+
public static async Task InitAsync(this IHost host, CancellationToken cancellationToken = default)
2022
{
2123
if (host == null)
2224
throw new ArgumentNullException(nameof(host));
2325

24-
using (var scope = host.Services.CreateScope())
26+
using var scope = host.Services.CreateScope();
27+
var rootInitializer = scope.ServiceProvider.GetService<RootInitializer?>();
28+
if (rootInitializer == null)
2529
{
26-
var rootInitializer = scope.ServiceProvider.GetService<RootInitializer?>();
27-
if (rootInitializer == null)
28-
{
29-
throw new InvalidOperationException("The async initialization service isn't registered, register it by calling AddAsyncInitialization() on the service collection or by adding an async initializer.");
30-
}
31-
32-
await rootInitializer.InitializeAsync();
30+
throw new InvalidOperationException("The async initialization service isn't registered, register it by calling AddAsyncInitialization() on the service collection or by adding an async initializer.");
3331
}
32+
33+
await rootInitializer.InitializeAsync(cancellationToken);
3434
}
3535
}
36-
}
36+
}

src/Extensions.Hosting.AsyncInitialization/IAsyncInitializer.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Threading.Tasks;
1+
using System.Threading;
2+
using System.Threading.Tasks;
23

34
namespace Extensions.Hosting.AsyncInitialization
45
{
@@ -10,7 +11,8 @@ public interface IAsyncInitializer
1011
/// <summary>
1112
/// Performs async initialization.
1213
/// </summary>
14+
/// <param name="cancellationToken">Notifies that the operation should be cancelled</param>
1315
/// <returns>A task that represents the initialization completion.</returns>
14-
Task InitializeAsync();
16+
Task InitializeAsync(CancellationToken cancellationToken);
1517
}
1618
}

src/Extensions.Hosting.AsyncInitialization/RootInitializer.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Threading;
34
using System.Threading.Tasks;
45
using Microsoft.Extensions.Logging;
56

@@ -16,18 +17,19 @@ public RootInitializer(ILogger<RootInitializer> logger, IEnumerable<IAsyncInitia
1617
_initializers = initializers;
1718
}
1819

19-
public async Task InitializeAsync()
20+
public async Task InitializeAsync(CancellationToken cancellationToken = default)
2021
{
2122
_logger.LogInformation("Starting async initialization");
2223

2324
try
2425
{
2526
foreach (var initializer in _initializers)
2627
{
28+
cancellationToken.ThrowIfCancellationRequested();
2729
_logger.LogInformation("Starting async initialization for {InitializerType}", initializer.GetType());
2830
try
2931
{
30-
await initializer.InitializeAsync();
32+
await initializer.InitializeAsync(cancellationToken);
3133
_logger.LogInformation("Async initialization for {InitializerType} completed", initializer.GetType());
3234
}
3335
catch (Exception ex)
@@ -39,7 +41,12 @@ public async Task InitializeAsync()
3941

4042
_logger.LogInformation("Async initialization completed");
4143
}
42-
catch(Exception ex)
44+
catch (OperationCanceledException)
45+
{
46+
_logger.LogWarning("Async initialization cancelled");
47+
throw;
48+
}
49+
catch (Exception ex)
4350
{
4451
_logger.LogError(ex, "Async initialization failed");
4552
throw;

tests/Extensions.Hosting.AsyncInitialization.Tests/AsyncInitializationTests.cs

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Threading;
23
using System.Threading.Tasks;
34
using FakeItEasy;
45
using Microsoft.Extensions.DependencyInjection;
@@ -18,19 +19,19 @@ public async Task Single_initializer_is_called()
1819

1920
await host.InitAsync();
2021

21-
A.CallTo(() => initializer.InitializeAsync()).MustHaveHappenedOnceExactly();
22+
A.CallTo(() => initializer.InitializeAsync(CancellationToken.None)).MustHaveHappenedOnceExactly();
2223
}
2324

2425
[Fact]
2526
public async Task Delegate_initializer_is_called()
2627
{
27-
var initializer = A.Fake<Func<Task>>();
28+
var initializer = A.Fake<Func<CancellationToken, Task>>();
2829

2930
var host = CreateHost(services => services.AddAsyncInitializer(initializer));
3031

3132
await host.InitAsync();
3233

33-
A.CallTo(() => initializer()).MustHaveHappenedOnceExactly();
34+
A.CallTo(() => initializer(CancellationToken.None)).MustHaveHappenedOnceExactly();
3435
}
3536

3637
[Fact]
@@ -49,9 +50,9 @@ public async Task Multiple_initializers_are_called_in_order()
4950

5051
await host.InitAsync();
5152

52-
A.CallTo(() => initializer1.InitializeAsync()).MustHaveHappenedOnceExactly()
53-
.Then(A.CallTo(() => initializer2.InitializeAsync()).MustHaveHappenedOnceExactly())
54-
.Then(A.CallTo(() => initializer3.InitializeAsync()).MustHaveHappenedOnceExactly());
53+
A.CallTo(() => initializer1.InitializeAsync(default)).MustHaveHappenedOnceExactly()
54+
.Then(A.CallTo(() => initializer2.InitializeAsync(default)).MustHaveHappenedOnceExactly())
55+
.Then(A.CallTo(() => initializer3.InitializeAsync(default)).MustHaveHappenedOnceExactly());
5556
}
5657

5758
[Fact]
@@ -75,7 +76,7 @@ public async Task Failing_initializer_makes_initialization_fail()
7576
var initializer2 = A.Fake<IAsyncInitializer>();
7677
var initializer3 = A.Fake<IAsyncInitializer>();
7778

78-
A.CallTo(() => initializer2.InitializeAsync()).ThrowsAsync(() => new Exception("oops"));
79+
A.CallTo(() => initializer2.InitializeAsync(default)).ThrowsAsync(() => new Exception("oops"));
7980

8081
var host = CreateHost(services =>
8182
{
@@ -88,8 +89,34 @@ public async Task Failing_initializer_makes_initialization_fail()
8889
Assert.IsType<Exception>(exception);
8990
Assert.Equal("oops", exception.Message);
9091

91-
A.CallTo(() => initializer1.InitializeAsync()).MustHaveHappenedOnceExactly();
92-
A.CallTo(() => initializer3.InitializeAsync()).MustNotHaveHappened();
92+
A.CallTo(() => initializer1.InitializeAsync(default)).MustHaveHappenedOnceExactly();
93+
A.CallTo(() => initializer3.InitializeAsync(default)).MustNotHaveHappened();
94+
}
95+
96+
[Fact]
97+
public async Task Cancelled_initializer_makes_initialization_fail()
98+
{
99+
using var cancellationTokenSource = new CancellationTokenSource();
100+
var initializer1 = A.Fake<IAsyncInitializer>();
101+
var initializer2 = A.Fake<IAsyncInitializer>();
102+
var initializer3 = A.Fake<IAsyncInitializer>();
103+
104+
105+
A.CallTo(() => initializer1.InitializeAsync(A<CancellationToken>._)).Invokes(_ => cancellationTokenSource.Cancel());
106+
107+
var host = CreateHost(services =>
108+
{
109+
services.AddAsyncInitializer(initializer1);
110+
services.AddAsyncInitializer(initializer2);
111+
services.AddAsyncInitializer(initializer3);
112+
});
113+
114+
var exception = await Record.ExceptionAsync(() => host.InitAsync(cancellationTokenSource.Token));
115+
Assert.IsType<OperationCanceledException>(exception);
116+
117+
A.CallTo(() => initializer1.InitializeAsync(A<CancellationToken>._)).MustHaveHappenedOnceExactly();
118+
A.CallTo(() => initializer2.InitializeAsync(A<CancellationToken>._)).MustNotHaveHappened();
119+
A.CallTo(() => initializer3.InitializeAsync(A<CancellationToken>._)).MustNotHaveHappened();
93120
}
94121

95122
[Fact]
@@ -125,7 +152,8 @@ public Initializer(IDependency dependency)
125152
{
126153
_dependency = dependency;
127154
}
128-
public Task InitializeAsync() => Task.CompletedTask;
155+
156+
public Task InitializeAsync(CancellationToken cancellationToken) => Task.CompletedTask;
129157
}
130158
}
131159
}

tests/Extensions.Hosting.AsyncInitialization.Tests/Extensions.Hosting.AsyncInitialization.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<TargetFrameworks>netcoreapp3.0;netcoreapp2.1;net471</TargetFrameworks>
5-
<LangVersion>8.0</LangVersion>
5+
<LangVersion>latest</LangVersion>
66
<Nullable>enable</Nullable>
77
<IsPackable>false</IsPackable>
88
<MSEHostingVersion Condition="'$(TargetFramework)' == 'netcoreapp2.1'">2.1.0</MSEHostingVersion>

0 commit comments

Comments
 (0)