How to cancel manually a BackgroundService in ASP.net core

Viewed 5478

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?

3 Answers

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();
    }
}

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.

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);
        }
    }
}
Related