Skip to content

Instantly share code, notes, and snippets.

@davidfowl
Created July 17, 2017 09:31
Show Gist options
  • Star 41 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save davidfowl/a7dd5064d9dcf35b6eae1a7953d615e3 to your computer and use it in GitHub Desktop.
Save davidfowl/a7dd5064d9dcf35b6eae1a7953d615e3 to your computer and use it in GitHub Desktop.
A base class that allows writing a long running background task in ASP.NET Core 2.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
namespace WebApplication24
{
public abstract class HostedService : IHostedService
{
private Task _executingTask;
private CancellationTokenSource _cts;
public Task StartAsync(CancellationToken cancellationToken)
{
// Create a linked token so we can trigger cancellation outside of this token's cancellation
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
// Store the task we're executing
_executingTask = ExecuteAsync(_cts.Token);
// If the task is completed then return it
if (_executingTask.IsCompleted)
{
return _executingTask;
}
// Otherwise it's running
return Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
// Stop called without start
if (_executingTask == null)
{
return;
}
// Signal cancellation to the executing method
_cts.Cancel();
// Wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));
// Throw if cancellation triggered
cancellationToken.ThrowIfCancellationRequested();
}
// Derived classes should override this and execute a long running method until
// cancellation is requested
protected abstract Task ExecuteAsync(CancellationToken cancellationToken);
}
}
@cwe1ss
Copy link

cwe1ss commented Jul 31, 2017

Hi David, isn't the linked CTS a problem here? As the original token from StartAsync() would trigger immediately when CTRL+C is pressed, this would result in ExecuteAsync being cancelled even before someone calls StopAsync(), right?

@davidfowl
Copy link
Author

@cwe1ss hey!

As the original token from StartAsync() would trigger immediately when CTRL+C is pressed. this would result in ExecuteAsync being cancelled even before someone calls StopAsync(), right?

The Token passed into StartAsync is to cancel startup (if it isn't that then we might have a problem) which makes it valid to cancel the entire process (even before StopAsync is called). One thing we haven't clarified is the behavior when StartAsync is cancelled, should it even call StopAsync?

@simeyla
Copy link

simeyla commented Jan 9, 2018

@davidfowl Did we clarify ;-) ?

@yaireclipse
Copy link

yaireclipse commented Mar 8, 2018

Hi David!
Thanks for providing this class :)
Two questions please:

  1. Shouldn't line 26 be switched with line 30, so Task.CompletedTask is returned when _executingTask.IsCompleted is true and _executingTask is returned otherwise?
  2. Shouldn't StartAsync be marked with the async modifier (like you did with StopAsync), and then lines 23-30 could apparently be replaced with await _executingTask;, and no return statements will be needed, as shown in this MSDN's example?

Thanks!

@yaireclipse
Copy link

Oh my, I think I got it. The return Task by StartAsync is the task of starting the internal _executingTask. If _executingTask.IsCompleted so fast, then it's probably due to a failure, and anyway it could be considered as if the task of starting _executingTask failed, and the reason for the failure might be encapsulated withing _executingTask. That's why when _executingTask.IsCompleted, _executingTask is returned. Otherwise, if _executingTask is still running, then the task of starting _executingTask has completed successfully and therefore Task.ComletedTask is returned...

This think this might also answer my second question. Since StartAsync doesn't really create a Task for starting _executingTask, it returns different instances of Task, depending on whether starting _executingTask succeeded or not. If it succeeded - some Task.ComletedTask is returned; otherwise, the internal _executingTask is returned, which is a bit of a trick to propagate the internal error that occurred in the attempt of starting _executingTask. This forces to not use the async modifier on StartAsync, as using async would inevitably return the first awaited Task, which would prevent using that trick.

When you get around to it, I'd love to know if I understood correctly.

Thanks!
Yair

@davidfowl
Copy link
Author

@vitidev
Copy link

vitidev commented May 7, 2019

Is cancellationToken.ThrowIfCancellationRequested(); in StopAsync necessary?

HostedServiceExecutor doesn't catch OperationCancelledException and we get a lot of errors in console

 Application started. Press Ctrl+C to shut down.
 Application is shutting down...
 crit: Microsoft.AspNetCore.Hosting.Internal.HostedServiceExecutor[10]
 An error occurred stopping the application
 System.AggregateException: One or more errors occurred. (The operation was canceled.)  (The operation was canceled.) (The operation was canceled.) ---> System.OperationCanceledException: The operation was canceled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment