8

Given the following .NET Core 2.2 console application that uses generic host:

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

namespace SimpleGenericHost
{
    class SimpleHostedService : IHostedService
    {
        public Task StartAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("Service started");
            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("Service stopped");
            return Task.CompletedTask;
        }
    }

    class Program
    {
        static async Task Main(string[] args)
        {
            var host = new HostBuilder()
                .ConfigureServices(services =>
                {
                    services.AddHostedService<SimpleHostedService>();
                })
                .Build();

            var runTask = host.RunAsync();
            await Task.Delay(5000);
            await host.StopAsync();
            await runTask;

        }
    }
}

When you run it, the following is output:

Service started
Application started. Press Ctrl+C to shut down.
Hosting environment: Production
Content root path: C:\projects\ConsoleApps\SimpleGenericHost\bin\Debug\netcoreapp2.2\
Service stopped
Service stopped

As you can see SimpleHostedService.StopAsync is called twice. Why?

Is this expected? am I missing something? Is there another way to stop the host that IHostedService.StopAsync is called just once?

Palle Due
  • 5,929
  • 4
  • 17
  • 32
Jesús López
  • 8,338
  • 7
  • 40
  • 66

1 Answers1

12

Because it was called twice - once after the delay and another time when the service actually stopped. It's not meant to be called when RunAsync is used.

To stop after a timeout, use a CancellationTokenSource with a timeout and pass its token to RunAsync :

var timeoutCts=new CancellationTokenSource(5000);

await host.RunAsync(timeoutCts.Token);

Explanation

StopAsync doesn't stop the service, it's used to notify the service that it needs to stop. It's called by the hosted service infrastructure itself when the application stops.

.NET Core is open source which means you can check the RunAsync source. RunAsync starts the host and then awaits for termination:

await host.StartAsync(token);

await host.WaitForShutdownAsync(token);

The WaitForShutdownAsync method listens for a termination request, either from the console or by an explicit call to IHostApplicationLifetime.StopApplication. When that happens it calls StopAsync itself :

await waitForStop.Task;

// Host will use its default ShutdownTimeout if none is specified.
await host.StopAsync();

You should use StartAsync instead of RunAsync if you intend to manage the application's lifetime yourself.

In this case though, you only need to stop the application if it times out. You can do that easily by passing a cancellation token to RunAsync that fires only after a timeout , through the CancellationTokenSource(int) constructor :

var timeoutCts=new CancellationTokenSource(5000);

await host.RunAsync(timeoutCts.Token);
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • I'm pretty sure that `IHostApplicationLifetime` is the netcore 3.0 renamed version of `IApplicationLifetime`, introduced due to naming conflicts between aspnetcore hosting and generic host. The methods/properties are the same, just a call out in case someone is trying to use the interface in 2.2 – pinkfloydx33 Aug 09 '19 at 09:07
  • Yes I know... I use generic host myself. I'm just saying I don't think the interface's name has changed yet, outside of the preview packages. Could be wrong, but I was view-sourcing some code yesterday and didn't notice the name-change – pinkfloydx33 Aug 09 '19 at 09:10
  • @pinkfloydx33 tracking the current source code is hard enough. I remember that change, it was a minor `who moved my cheese` moment – Panagiotis Kanavos Aug 09 '19 at 09:12
  • @PanagiotisKanavos. Thank you very much, both CancellationTokenSource and IApplicationLifetime.StopApplication work for me. No need to use StartAsync to manage appliucations's lifetime myself. – Jesús López Aug 09 '19 at 09:21
  • @PanagiotisKanavos Just to clarify, if you use `RunAsync`, does that mean you have to use an `IHostedService`? I think I'd rather manage the lifetime rather than have to break out the guts of my program into a service. (That's assuming `RunAsync` is a blocking call, which is seems to be). Nicer approach [here](https://stackoverflow.com/a/66996436/540156) – onefootswill Feb 17 '22 at 07:50
  • 1
    @onefootswill no, `Run` or `RunAsync` actually start the application and all services. In an ASP.NET Core application for example, nothing starts running until `Run` is called. This isn't a matter of approaches - what do you thing `Run` calls under the covers? `host.Start.Async()`. How do you signal the application to stop in *every* case? You call `StopApplication`. In fact, I already link to the source code that shows the same calls are made as in the linked answer – Panagiotis Kanavos Feb 17 '22 at 07:54
  • @PanagiotisKanavos Fair enough. I have just observed that none of my code runs until I kill the application with `Ctrl-C` (if that code comes after the invocation of Run). That's why I assumed `Run` was blocking. That is what started me on this investigation. I'd call `host.Run()` and nothing would happen (until Ctrl-C). – onefootswill Feb 17 '22 at 08:01
  • 1
    @onefootswill in fact, there's no need to call `Run` or `StartApplication` in a console app at all. Just use the host to retrieve services and configuration and simply exit your main function when done. – Panagiotis Kanavos Feb 17 '22 at 08:01
  • @PanagiotisKanavos that makes sense and is what I have done in Winforms apps. – onefootswill Feb 17 '22 at 08:02
  • 1
    @onefootswill `(if that code comes after the invocation of Run).` that's because `Run` is what runs the application. It exits when the application terminates. Any code after `Run` is invoked during termination – Panagiotis Kanavos Feb 17 '22 at 08:03
  • 1
    @onefootswill you need `Run(Async)` when you actually need something to host and run your application. A WinForms application doesn't need another host, it already hosts itself. All other services, eg DI, Configuration, Logging, can be used independently. The Generic Host is a convenient place to access all of them though. – Panagiotis Kanavos Feb 17 '22 at 08:07
  • @PanagiotisKanavos Thanks. That's clarified a few things. Cheers. – onefootswill Feb 17 '22 at 08:14