Skip to content

Commit 963ff97

Browse files
committed
More docs, .NET 5 compatibility(.NET 6 stops the host on exception)
1 parent dc238f5 commit 963ff97

File tree

6 files changed

+142
-11
lines changed

6 files changed

+142
-11
lines changed

.github/workflows/Publish-Package.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ jobs:
2323
dotnet-version: 5.0.x
2424

2525
- name: Restore dependencies
26-
run: dotnet restore
26+
run: dotnet restore CodeCaster.WindowsServiceExtensions.sln
2727

2828
- name: Build
29-
run: dotnet build --no-restore --configuration=Release -p:Version=${{ steps.git_version.outputs.version-without-v }}
29+
run: dotnet build CodeCaster.WindowsServiceExtensions.sln --no-restore --configuration=Release -p:Version=${{ steps.git_version.outputs.version-without-v }}
3030

3131
- name: Release to GitHub
3232
uses: softprops/action-gh-release@v1

docs/index.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public class MyCoolBackgroundService : BackgroundService
4141
}
4242
```
4343

44-
As long as your `ExecuteAsync()` runs, you have a _.NET_ (not Widows!) background service (`IHostedService`) running. When a hosted service throws an exception, that will stop the .NET Host that runs your application, and an event will be logged (as long as it exists and/or permisions are adequate).
44+
As long as your `ExecuteAsync()` runs, you have a _.NET_ (not Widows!) background service (`IHostedService`) running. When service start immediately throws an exception, that will stop the .NET Host that runs your application, and an event will be logged (as long as it exists and/or permisions are adequate).
4545

4646
## Lifetime
4747
So far, so good, but...
@@ -67,14 +67,39 @@ var hostBuilder = new HostBuilder()
6767
.UseWindowsServiceExtensions();
6868
```
6969

70+
## TryExecuteAsync
71+
This library seals `Microsoft.Extensions.Hosting.BackgroundService.ExecuteAsync()`, to stop the host when an exception occurs in a background service. To apply this to your code, inherit from `CodeCaster.WindowsServiceExtensions.HostTerminatingBackgroundService` instead of [`Microsoft.Extensions.Hosting.BackgroundService`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice?view=dotnet-plat-ext-5.0).
72+
73+
```csharp
74+
public class MyCoolBackgroundService : BackgroundService
75+
{
76+
private readonly ILogger<MyCoolBackgroundService> _logger;
77+
78+
public MyCoolBackgroundService(ILogger<MyCoolBackgroundService> logger, IHostApplicationLifetime applicationLifetime)
79+
: base(logger, applicationLifetime)
80+
{
81+
_logger = logger;
82+
}
83+
84+
protected override async Task TryExecuteAsync(CancellationToken stoppingToken)
85+
{
86+
// Fake doing at least some work...
87+
await Task.Delay(1000, stoppingToken);
88+
89+
// This will now stop the host application.
90+
throw new InvalidOperationException("This service is not supposed to start");
91+
}
92+
}
93+
```
94+
7095
## Power events
71-
If you let your service inherit `PowerEventAwareBackgroundService` instead of [`Microsoft.Extensions.Hosting.BackgroundService`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice?view=dotnet-plat-ext-5.0) (the former inherits the latter), you get a new method:
96+
If you let your service inherit `CodeCaster.WindowsServiceExtensions.PowerEventAwareBackgroundService` instead of [`Microsoft.Extensions.Hosting.BackgroundService`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice?view=dotnet-plat-ext-5.0) (the former indirectly inherits the latter, see above), you get a new method:
7297

7398
```C#
7499
public class MyCoolBackgroundService : PowerEventAwareBackgroundService
75100
{
76101
// This still runs your long-running background job
77-
protected override Task ExecuteAsync(CancellationToken stoppingToken)
102+
protected override Task TryExecuteAsync(CancellationToken stoppingToken)
78103
{
79104
// Do your continuous or periodic background work.
80105
await SomeLongRunningTaskAsync();

src/CodeCaster.WindowsServiceExtensions/CodeCaster.WindowsServiceExtensions.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@
2525
</PropertyGroup>
2626

2727
<ItemGroup>
28-
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
28+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
2929

30-
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
31-
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="5.0.1" />
30+
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
31+
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
3232
</ItemGroup>
3333

3434
</Project>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Microsoft.Extensions.Hosting;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace CodeCaster.WindowsServiceExtensions
8+
{
9+
/// <summary>
10+
/// Terminates the host application when an exception occurs in <see cref="ExecuteAsync"/>.
11+
/// </summary>
12+
public abstract class HostTerminatingBackgroundService : BackgroundService
13+
{
14+
private readonly ILogger<HostTerminatingBackgroundService> _logger;
15+
private readonly IHostApplicationLifetime _applicationLifetime;
16+
17+
/// <inheritdoc/>
18+
protected HostTerminatingBackgroundService(ILogger<HostTerminatingBackgroundService> logger, IHostApplicationLifetime applicationLifetime)
19+
{
20+
_logger = logger;
21+
_applicationLifetime = applicationLifetime;
22+
}
23+
24+
/// <inheritdoc />
25+
protected sealed override async Task ExecuteAsync(CancellationToken stoppingToken)
26+
{
27+
try
28+
{
29+
await TryExecuteAsync(stoppingToken);
30+
}
31+
catch (Exception e)
32+
{
33+
var errorString = $"Unhandled exception in {GetType().FullName}.ExecuteAsync()";
34+
35+
_logger.LogError(e, errorString);
36+
37+
// .NET 6+ terminates the host on exception, 5 doesn't. Do it ourselves.
38+
// https://docs.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/6.0/hosting-exception-handling
39+
Environment.ExitCode = -1;
40+
41+
_applicationLifetime.StopApplication();
42+
43+
throw new InvalidOperationException(errorString, e);
44+
}
45+
}
46+
47+
/// <summary>
48+
/// Calls <see cref="BackgroundService.ExecuteAsync"/> in a try-catch block, stopping the host application when it throws.
49+
/// </summary>
50+
protected abstract Task TryExecuteAsync(CancellationToken stoppingToken);
51+
}
52+
}

src/CodeCaster.WindowsServiceExtensions/PowerEventAwareBackgroundService.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
using System.ServiceProcess;
22
using Microsoft.Extensions.Hosting;
3+
using Microsoft.Extensions.Logging;
34

45
namespace CodeCaster.WindowsServiceExtensions
56
{
67
/// <summary>
78
/// Base class for implementing a long running <see cref="IHostedService"/> as a Windows Service that can react to power state changes.
89
/// </summary>
9-
public abstract class PowerEventAwareBackgroundService : BackgroundService, IPowerEventAwareHostedService
10+
public abstract class PowerEventAwareBackgroundService : HostTerminatingBackgroundService, IPowerEventAwareHostedService
1011
{
12+
/// <inheritdoc />
13+
protected PowerEventAwareBackgroundService(ILogger<HostTerminatingBackgroundService> logger, IHostApplicationLifetime applicationLifetime)
14+
: base(logger, applicationLifetime)
15+
{
16+
}
17+
1118
/// <summary>
1219
/// Override this method to react to power state changes.
1320
/// </summary>

test/TestServiceThatThrows/Program.cs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,46 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
6868
throw new InvalidOperationException("This service is not supposed to start");
6969
}
7070
}
71+
72+
public class MyHostTerminatingBackgroundService : HostTerminatingBackgroundService
73+
{
74+
private readonly ILogger<MyHostTerminatingBackgroundService> _logger;
75+
76+
public MyHostTerminatingBackgroundService(ILogger<MyHostTerminatingBackgroundService> logger, IHostApplicationLifetime applicationLifetime)
77+
: base(logger, applicationLifetime)
78+
{
79+
_logger = logger;
80+
}
81+
82+
protected override async Task TryExecuteAsync(CancellationToken stoppingToken)
83+
{
84+
// Fake doing at least some work...
85+
await Task.Delay(1000, stoppingToken);
86+
87+
// This will now stop the host application.
88+
throw new InvalidOperationException("This service is not supposed to start");
89+
}
90+
}
91+
92+
public class MyHostTerminatingPowerEventAwareBackgroundService : HostTerminatingBackgroundService
93+
{
94+
private readonly ILogger<MyHostTerminatingPowerEventAwareBackgroundService> _logger;
95+
96+
public MyHostTerminatingPowerEventAwareBackgroundService(ILogger<MyHostTerminatingPowerEventAwareBackgroundService> logger, IHostApplicationLifetime applicationLifetime)
97+
: base(logger, applicationLifetime)
98+
{
99+
_logger = logger;
100+
}
101+
102+
protected override async Task TryExecuteAsync(CancellationToken stoppingToken)
103+
{
104+
// Fake doing at least some work...
105+
await Task.Delay(1000, stoppingToken);
106+
107+
// This will now stop the host application.
108+
throw new InvalidOperationException("This service is not supposed to start");
109+
}
110+
}
71111

72112
public class MyHappyBackgroundService : BackgroundService
73113
{
@@ -109,7 +149,7 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken)
109149

110150
public static class Program
111151
{
112-
public static async Task Main(string[] args)
152+
public static async Task Main()
113153
{
114154
if (!Debugger.IsAttached)
115155
{
@@ -126,9 +166,16 @@ public static async Task Main(string[] args)
126166
s.AddHostedService<MyHappyBackgroundService>();
127167
s.AddHostedService<QuicklyQuittingBackgroundService>();
128168

169+
// This one breaks in OnStart().
129170
//s.AddHostedService<MyFaultyService>();
130-
s.AddHostedService<MyFaultyBackgroundService>();
131171

172+
// This one throws after 500ms but doesn't take down the host.
173+
//s.AddHostedService<MyFaultyBackgroundService>();
174+
175+
// This one should stop the application.
176+
//s.AddHostedService<MyHostTerminatingBackgroundService>();
177+
s.AddHostedService<MyHostTerminatingPowerEventAwareBackgroundService>();
178+
132179
//throw new InvalidOperationException("Heh");
133180
})
134181
//.UseWindowsService()

0 commit comments

Comments
 (0)