13

I create a BackgroundService like this:

public class CustomService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        do
        {
            //...

            await Task.Delay(60000, cancellationToken);
        }
        while (!cancellationToken.IsCancellationRequested);
    }
}

How to cancel it manually?

Mehdi
  • 903
  • 3
  • 10
  • 26
  • You can get a reference to it; `.GetService>().OfType().Single().MethodName(....)`. (or inject some other common service to indicate cancellation ... ) – Jeremy Lakeman Jun 09 '21 at 06:56
  • This way came to my mind, but I want to know if it can not be done through cancellationToken? – Mehdi Jun 09 '21 at 07:19
  • You don't need a reference to `IHostedService`. `BackgroundService` **is** an IHostedSevice. What do you mean `cancel it manually` though? Cancel all background services explicitly, and perhaps the application itself? Cancel one service but leave the others running? – Panagiotis Kanavos Jun 09 '21 at 09:05
  • @PanagiotisKanavos If you want some other service to locate your `BackgroundService` instance, you will need to do something like my sample above. If you register the service in another way, you'll have multiple instances. – Jeremy Lakeman Jun 10 '21 at 00:59

3 Answers3

18

It's unclear whether you want to cancel all services and maybe the application itself (or at least the host), or just a single service.

Stopping the application

To cancel the application, inject the IHostApplicationLifetime interface in the class that will force the cancellation and call StopApplication when needed. If you want to cancel from inside the background service itself, perhaps because there's nothing else to do, that's where you need to inject.

StopApplication will tell the host the application needs to shut down. The host will call StopAsync on all hosted services. Since you use BackgroundService, the implementation will trigger the cancellationToken passed to ExecuteAsync:

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        // Stop called without start
        if (_executeTask == null)
        {
            return;
        }

        try
        {
            // Signal cancellation to the executing method
            _stoppingCts.Cancel();
        }
        finally
        {
            // Wait until the task completes or the stop token triggers
            await Task.WhenAny(_executeTask, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(false);
        }

    }

You don't have to change your current code at all. The only concern is that await Task.Delay() leaks timers. It would be better to use a Timer explicitly, and dispose it when cancellation is triggered.

For example, if you want to shut down the application from a controller action:

public class MyServiceControllerr:Controller
{
    IHostApplicationLifetime _lifetime;
    public MyServiceController(IHostApplicationLifetime lifeTime)
    {
        _lifeTime=lifeTime;
    }

    [HttpPost]
    public IActionResult Stop()
    {
        _lifeTime.StopApplication();
        return Ok();
    }
}

Stopping the service

If you want to stop just this one service, you need a way to call its StopAsync method from some other code. There are numerous ways to do this. One such way is to inject CustomService to the caller and call StopAsync. That's not a very good idea though, as it exposes the service and couples the controller/stopping code with the service. Testing this won't be easy either.

Another possibility is to create an interface just for the call to StopAsync, eg :

public interface ICustomServiceStopper
{
    Task StopAsync(CancellationToken token=default);
}

public class CustomService : BackgroundService,ICustomServiceStopper
{
    ...

    Task ICustomServiceStopper.StopAsync(CancellationToken token=default)=>base.StopAsync(token);
    
}

Register the interface as a singleton:

services.AddSingleton<ICustomServiceStopper,CustomService>();

and inject ICustomServiceStopper when needed:

public class MyServiceControllerr:Controller
{
    ICustomServiceStopper _stopper;
    public MyServiceController(ICustomServiceStopper stopper)
    {
        _stopper=stopper;
    }

    [HttpPost]
    public async Task<IActionResult> Stop()
    {
        await _stopper.StopAsync();
        return Ok();
    }
}
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • As far as I know all BackgroundService closes automatically when the app closes, right? – Mehdi Jun 10 '21 at 06:23
2

It's late, but simply trigger StopAsync(CancellationToken cancellationToken) method, which is part of BackgroundService interface, it will only stop your current worker, not the whole app:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    var stopCounter = 10;
    while (!stoppingToken.IsCancellationRequested)
    {
        _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
        await Task.Delay(1000, stoppingToken);
        stopCounter--;
        if (stopCounter < 1)
        {
            StopAsync(stoppingToken);
        }
    }
}
sryv
  • 29
  • 1
0

I've checked the BackgroundService source code

and it looks like my previous answer was wrong.

The ExecuteAsync parameter is a token provided by the StartAsync method.

The BackgroundService token source is cancelled by the StopAsync method.

So to cancel the CustomService async work you have to call the StopAsync method. This cancel token provided to the ExecuteAsync method as parameter.

oleksa
  • 3,688
  • 1
  • 29
  • 54
  • 1
    The OP already uses the CancellationToken. The question is how to trigger cancellation in the first place – Panagiotis Kanavos Jun 09 '21 at 09:02
  • @PanagiotisKanavos the OP uses cancellation token but is asking how to cancel. This looks a little bit wierd so I've shown two options how to cancel the Background service successor. – oleksa Jun 09 '21 at 09:09
  • 1
    It's not weird at all - that's what happens when eg `Ctrl+C` is used in a service worker. It's not strange to want to explicitly stop a service either. What you posted doesn't do what you assume it does. `StopAsync` is already implemented in `BackgroundService`. It's used to *respond* to cancellation, not trigger it – Panagiotis Kanavos Jun 09 '21 at 09:11