rewrite an IHostedService to stop after all tasks finished
T

5

5

I have an application that normally should be a simple console application to be programmed as a scheduled task from time to time called by the windows task scheduler.

The program should launch some updates on two databases, one service per one database. Say ContosoDatabase should be updated by the ContosoService.

Finally it was written as an .NET Core app using, and maybe is not the best choice, the IHostedServices as base for the service, like this:

public class ContosoService : IHostedService {
    private readonly ILogger<ContosoService> _log;
    private readonly IContosoRepository _repository;
    
    private Task executingTask;

    public ContosoService(
        ILogger<ContosoService> log,
        IContosoRepository repository,
        string mode) {
        _log = log;
        _repository = repository;
    }

    public Task StartAsync(CancellationToken cancellationToken) {
        _log.LogInformation(">>> {serviceName} started <<<", nameof(ContosoService));
        executingTask = ExcecuteAsync(cancellationToken);

        // If the task is completed then return it, 
        // this should bubble cancellation and failure to the caller
        if (executingTask.IsCompleted)
            return executingTask;

        // Otherwise it's running
        // >> don't want it to run!
        // >> it should end after all task finished!
        return Task.CompletedTask;
    }

    private async Task<bool> ExcecuteAsync(CancellationToken cancellationToken) {
        var myUsers = _repository.GetMyUsers();

        if (myUsers == null || myUsers.Count() == 0) {
            _log.LogWarning("{serviceName} has any entry to process, will stop", this.GetType().Name);
            return false;
        }
        else {
            // on mets à jour la liste des employés Agresso obtenue
            await _repository.UpdateUsersAsync(myUsers);
        }

        _log.LogInformation(">>> {serviceName} finished its tasks <<<", nameof(ContosoService));
        return true;
    }

    public Task StopAsync(CancellationToken cancellationToken) {
        _log.LogInformation(">>> {serviceName} stopped <<<", nameof(ContosoService));
        return Task.CompletedTask;
    }
}

and I call it from main like this:

public static void Main(string[] args)
{
    try {
        CreateHostBuilder(args).Build().Run();
    }
    catch (Exception ex) {
        Log.Fatal(ex, ">>> the application could not start <<<");
    }
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host
    .CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) => {
        var config = hostContext.Configuration;
        
        if (args.Contains("Alonso")) {
            services
            .AddHostedService(provider =>
                new AlonsoService(
                    provider.GetService<ILogger<AlonsoService>>(),
                    provider.GetService<IAlonsoRepository>()));
        }

        // if there also Cedig in the list, they can be run in parallel
        if (args.Contains("Contoso")) {
            services
            .AddHostedService(provider =>
                new ContosoService(
                    provider.GetService<ILogger<ContosoService>>(),
                    provider.GetService<IContosoRepository>()));
        }
    });

Now, the problem, is surely, that the application will not stop once all updates finished.

Is there a way to quickly rewrite the application in order to make it stop after the second service finishes its tasks?

I tried to put the Environment.Exit(0); at the end

public static void Main(string[] args) {
    try {
        CreateHostBuilder(filteredArgs.ToArray()).Build().Run();                
    }
    catch (Exception ex) {
        //Log....
    }

    Environment.Exit(0); // here
}

but it does not seem to help: the application is still running after all task are completed.

Tinworks answered 9/2, 2021 at 10:18 Comment(0)
M
1

HostedServices are background services. It's the other way around: they can react to application start and stop events, so that they can end gracefully. They are not meant to stop your main application when finished, they potentially live as long as the application does.

I'd say you will be better served with simple Tasks and awaiting all of them. Or send some events when your background jobs finishes its work and handle them in main.

Whatever trigger you may choose you can stop .net app by injecting IHostApplicationLifetime and calling StopApplication() method on it. In earlier versions it's just IApplicationLifetime.

Martsen answered 9/2, 2021 at 10:57 Comment(4)
how do I send a message from my service to the main app to stop it?Tinworks
or, rather, I would like to stop the app when all services are completed, how to do it?Tinworks
IHostApplicationLifetime did the job, I just killed the application from the last executing service, spasibo bol'shoe ;)Tinworks
@garkushin's answer is the correct one. https://mcmap.net/q/1957964/-rewrite-an-ihostedservice-to-stop-after-all-tasks-finishedGraycegrayheaded
T
4

Following @Maxim's suggestion, I found this dirty but working workaround, by injecting the IHostApplicationLifetime and the lastService boolean:

public ConsosoService(
    IHostApplicationLifetime hostApplicationLifetime,
    // ...
    bool lastService) 
{ ... }

public async Task StartAsync(CancellationToken cancellationToken)
{
    // do the job

    if (_lastService)
        _hostApplicationLifetime.StopApplication(); 
    // stops the application and cancel/stops other services as well
}
Tinworks answered 9/2, 2021 at 17:39 Comment(2)
I know that you solved the issue, but I noticed that StopAsync() was not called anywhere. I think if it was put in the same place instead of _hostApplicationLifetime.StopApplication(); it will make the same result.Minimize
it seems that StopAsync is called by the application or the host when it tries to stop the application. I had logs from StopAsync passing after the StopApplication() callTinworks
I
3

You don't need to use IHost.Run(), but IHost.Start()

Ignaz answered 24/1 at 7:52 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Peterson
M
1

HostedServices are background services. It's the other way around: they can react to application start and stop events, so that they can end gracefully. They are not meant to stop your main application when finished, they potentially live as long as the application does.

I'd say you will be better served with simple Tasks and awaiting all of them. Or send some events when your background jobs finishes its work and handle them in main.

Whatever trigger you may choose you can stop .net app by injecting IHostApplicationLifetime and calling StopApplication() method on it. In earlier versions it's just IApplicationLifetime.

Martsen answered 9/2, 2021 at 10:57 Comment(4)
how do I send a message from my service to the main app to stop it?Tinworks
or, rather, I would like to stop the app when all services are completed, how to do it?Tinworks
IHostApplicationLifetime did the job, I just killed the application from the last executing service, spasibo bol'shoe ;)Tinworks
@garkushin's answer is the correct one. https://mcmap.net/q/1957964/-rewrite-an-ihostedservice-to-stop-after-all-tasks-finishedGraycegrayheaded
M
1

Looking at IHost Interface documentation the method run() does not stop until the host is shutdown. seems that StopAsync() did not stop the service. so Environment.Exit(0); was never reached. maybe use CancellationToken to forcefully end the host, or inject Environment.Exit(0); in ContosoService class if possible even though not optimal.

Minimize answered 9/2, 2021 at 16:7 Comment(2)
use CancellationTocker to forcefully stop the host, would be great... now wondering how to do it from inside the application....Tinworks
please see my own answer, thank you and thank @maxim :) !Tinworks
M
0

Here is another approach without need for creating hosted service

using var host = CreateHostBuilder(args).Build();
await host.StartAsync();
using var scope = host.Services.CreateScope();
var worker = scope.ServiceProvider.GetService<Worker>();
await worker!.Run();
await host.StopAsync();

IHostBuilder CreateHostBuilder(string[] args) =>
     Host.CreateDefaultBuilder(args)
         .ConfigureServices(services => ConfigureServices(services));

void ConfigureServices(IServiceCollection services)
{
    //main class which does the work
    services.AddScoped<Worker>();
    //do some DB operations
    services.AddScoped<DbCtxt>();
}

Complete code https://github.com/raghavan-mk/dotnet/tree/main/DIInConsole

Machutte answered 20/11, 2021 at 16:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.