Why my BackgroundService stops immediately?

Viewed 2379

I'm using .NET Core 2.2, and I would like to run a long time running background task (like the old Windows services). I would like to use Microsoft.Extensions.Hosting.BackgroundService (in Microsoft.Extensions.Hosting.Abstractions assembly).

My problem is that my service starts and stops immediately. I wrote this little sample to show what I've made:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace MyHostedService
{
    internal static class Program
    {
        private static void Main()
        {
            new HostBuilder()
                .ConfigureServices((hostContext, services) => { services.AddHostedService<MyService>(); })
                .Build()
                .RunAsync();
        }
    }
    public class MyService : BackgroundService
    {
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            int count = 0;
            while (!stoppingToken.IsCancellationRequested)
            {
                Console.WriteLine($"Hit count: {++count}, IsCancellationRequested: {stoppingToken.IsCancellationRequested}");
                await Task.Delay(TimeSpan.FromSeconds(1));
            }
            Console.WriteLine($"IsCancellationRequested: {stoppingToken.IsCancellationRequested}");
            Console.WriteLine("The end.");
        }
    }
}

And here is the output:

> Hit count: 1, IsCancellationRequested: False
Application started. Press Ctrl+C to shut down.
Hosting environment: Production
Content root path: C:\Users\(personnal path hidden)\MyHostedService\MyHostedService\bin\Debug\netcoreapp2.2\
> IsCancellationRequested: True
> The end.

C:\Program Files\dotnet\dotnet.exe (process 31088) exited with code 0.
To automatically close the console when debugging stops, enable Tools->Options->Debugging->Automatically close the console when debugging stops.
Press any key to close this window . . .

I didn't press Ctrl+C, nor escape key, I promise.

Do I miss something? Could you tell me what to change to ensure the service will run until I stop it by pressing Ctrl+C?

I expect to get a result that looks like:

Application started. Press Ctrl+C to shut down.
Hosting environment: Production
Content root path: C:\Users\(personnal path hidden)\MyHostedService\MyHostedService\bin\Debug\netcoreapp2.2\
> Hit count: 1, IsCancellationRequested: False
> Hit count: 2, IsCancellationRequested: False
> Hit count: 3, IsCancellationRequested: False
> Hit count: 4, IsCancellationRequested: False
> Hit count: 5, IsCancellationRequested: False
> Hit count: 6, IsCancellationRequested: False
> Hit count: 7, IsCancellationRequested: False
> Hit count: 8, IsCancellationRequested: False
> Hit count: 9, IsCancellationRequested: False
> Hit count: 10, IsCancellationRequested: False
> Hit count: 11, IsCancellationRequested: False

... ( now I press Ctrl+C ) ...

> IsCancellationRequested: True
> The end.

C:\Program Files\dotnet\dotnet.exe (process 31088) exited with code 0.
To automatically close the console when debugging stops, enable Tools->Options->Debugging->Automatically close the console when debugging stops.
Press any key to close this window . . .
2 Answers

You run it Asynchronously, so once your main function reaches the end, application exits. That's where cancellation is requested. Try to await the service:

private async Task Main()
{
    await new HostBuilder()
        .ConfigureServices((hostContext, services) => { services.AddHostedService<MyService>(); })
        .Build()
        .RunAsync();
}

It's not just the service, the entire application exits because nothing awaits the task generated by RunAsync() and Main simply exits immediatelly.

Main() should change to :

static async Task Main()
{
    await (new HostBuilder()
        .ConfigureServices((hostContext, services) => { services.AddHostedService<MyService>(); })
        .Build()
        .RunAsync());
}

Better yet :

static async Task Main()
{
    var host=new HostBuilder()
            .ConfigureServices((hostContext, services) => { services.AddHostedService<MyService>(); })
            .Build();
    await host.RunAsync();
}

Chaining calls isn't necessarily a good idea and the IHost instance returned by HostBuilder can be used to stop the application too, eg in response to a timeout

Another possible improvement is to use Host.CreateDefaultBuilder which adds several common configuration, logging settings based on command-line arguments :

static async Task Main(string[] args)
{
    var host=Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) => { services.AddHostedService<MyService>(); })
            .Build();
    await host.RunAsync();
}

Even in this simple case it means the BackgroundService can have somewhere to log without extra configuration

public class MyService : BackgroundService
{
    readonly ILogger<MyService> _logger;

    public MyService(ILogger<MyService> logger)
    {
        _logger=logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        int count = 0;
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInfo($"Hit count: {++count}, IsCancellationRequested: {stoppingToken.IsCancellationRequested}");
            await Task.Delay(TimeSpan.FromSeconds(1));
        }
        _logger.LogInfo($"IsCancellationRequested: {stoppingToken.IsCancellationRequested}");
        _logger.LogInfo("The end.");
    }
}
Related