3

Within a WPF application I have configured a hosted service to perform specific activity in background (Followed this article). This is how hosted service is configured in App.xaml.cs.

public App()
        {
            var environmentName = Environment.GetEnvironmentVariable("HEALTHBOOSTER_ENVIRONMENT") ?? "Development";
            IConfigurationRoot configuration = SetupConfiguration(environmentName);
            ConfigureLogger(configuration);
            _host = Host.CreateDefaultBuilder()
                .UseSerilog()
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<Worker>()
                    .AddOptions()
                    .AddSingleton<IMailSender, MailSender>()
                    .AddSingleton<ITimeTracker, TimeTracker>()
                    .AddSingleton<NotificationViewModel, NotificationViewModel>()
                    .AddTransient<NotificationWindow, NotificationWindow>()
                    .Configure<AppSettings>(configuration.GetSection("AppSettings"));
                }).Build();

            AssemblyLoadContext.Default.Unloading += Default_Unloading;

            Console.CancelKeyPress += Console_CancelKeyPress;

            SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
        }

And started on startup

/// <summary>
    /// Handles statup event
    /// </summary>
    /// <param name="e"></param>
    protected override async void OnStartup(StartupEventArgs e)
    {
        try
        {
            Log.Debug("Starting the application");
            await _host.StartAsync(_cancellationTokenSource.Token);
            base.OnStartup(e);
        }
        catch (Exception ex)
        {
            Log.Error(ex, "Failed to start application");
            await StopAsync();
        }
    }

Now I want to stop the hosted service when the system goes to sleep and restart the service when the system resumes. I tried this

/// <summary>
    /// Handles system suspend and resume events
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private async void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
    {
        switch (e.Mode)
        {
            case PowerModes.Resume:
                Log.Warning("System is resuming. Restarting the host");
                try
                {
                    _cancellationTokenSource = new CancellationTokenSource();
                    await _host.StartAsync(_cancellationTokenSource.Token);
                }
                catch (Exception ex)
                {
                    Log.Error(ex, $"{ex.Message}");
                }
                break;

            case PowerModes.Suspend:
                Log.Warning("System is suspending. Canceling the activity");
                _cancellationTokenSource.Cancel();
                await _host.StopAsync(_cancellationTokenSource.Token);
                break;
        }
    }

Stopping the host is working fine But when the host is restarted, I am getting 'System.OperationCanceledException'. As per my understanding hosted service lifetime is independent of application lifetime. Is my understanding wrong?

This question- ASP.NET Core IHostedService manual start/stop/pause(?) is similar but the answer is to pause and restart the service based on configuration which seems like a hack so I am looking for a standard way.

Any thoughts?

shreesha
  • 1,811
  • 2
  • 21
  • 30
  • 1
    Move the code that calls `Host.CreateDefaultBuilder()` and initializes the host to the `OnStartup` method in order to create a new host each time you want to restart it. You'll find an example [here](https://stackoverflow.com/a/60153020/7252182). – mm8 Apr 29 '20 at 12:56
  • 1
    I believe hosts are designed to only run once. You might be able to stop and start your background service, though. `Worker` has its own start and stop that is (mostly) independent from the host start and stop. – Stephen Cleary Apr 29 '20 at 13:13

1 Answers1

6

As Stephen Cleary noted in comments, workers can be started and stopped independent of the host though ideally host should handle the lifetime of the workers.
But due to an existing bug in .NET Core 3, cancellation token passed to IHostedService StartAsync method is not propagated to workers ExecuteAsync method. I have created an issue for the same and the details can be found here - https://github.com/dotnet/extensions/issues/3218

The fix to the bug (https://github.com/dotnet/extensions/pull/2823) will be part of .NET 5 so as suggested in the issue (https://github.com/dotnet/extensions/issues/3218#issuecomment-622503957) I had to create my own class to imitate framework's BackGroundService class and this class will directly inherit from IHostedService and propagate the cancellation token to workers.

BackGroundService class custom implementation is here -

/// <summary>
    /// Base class for implementing a long running <see cref="IHostedService"/>.
    /// </summary>
    public abstract class BGService : IHostedService, IDisposable
    {
        private Task _executingTask;
        private CancellationTokenSource _stoppingCts;

        /// <summary>
        /// This method is called when the <see cref="IHostedService"/> starts. The implementation should return a task that represents
        /// the lifetime of the long running operation(s) being performed.
        /// </summary>
        /// <param name="stoppingToken">Triggered when <see cref="IHostedService.StopAsync(CancellationToken)"/> is called.</param>
        /// <returns>A <see cref="Task"/> that represents the long running operations.</returns>
        protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

        /// <summary>
        /// Triggered when the application host is ready to start the service.
        /// </summary>
        /// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
        public virtual Task StartAsync(CancellationToken cancellationToken)
        {
            // Create linked token to allow cancelling executing task from provided token
            _stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

            // Store the task we're executing
            _executingTask = ExecuteAsync(_stoppingCts.Token);

            // If the task is completed then return it, this will bubble cancellation and failure to the caller
            if (_executingTask.IsCompleted)
            {
                return _executingTask;
            }

            // Otherwise it's running
            return Task.CompletedTask;
        }

        /// <summary>
        /// Triggered when the application host is performing a graceful shutdown.
        /// </summary>
        /// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param>
        public virtual async Task StopAsync(CancellationToken cancellationToken)
        {
            // Stop called without start
            if (_executingTask == 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(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
            }

        }

        public virtual void Dispose()
        {
            _stoppingCts?.Cancel();
        }
    }

And the logic to stop and start workers when system suspends and resumes respectively

/// <summary>
        /// Handles system suspend and resume events
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
        {
            var workers = _host.Services.GetServices<IHostedService>();
            Log.Information($"Found IHostedService instances - {workers.ToCSV()}");
            switch (e.Mode)
            {
                case PowerModes.Resume:
                    Log.Warning("System is resuming. Restarting the workers");
                    try
                    {
                        _cancellationTokenSource = new CancellationTokenSource();
                        foreach (var worker in workers)
                        {
                            await worker.StartAsync(_cancellationTokenSource.Token);
                        }
                    }
                    catch (Exception ex)
                    {
                        Log.Error(ex, $"{ex.Message}");
                    }
                    break;

                case PowerModes.Suspend:
                    Log.Warning("System is suspending. Stopping the workers");
                    _cancellationTokenSource.Cancel();
                    try
                    {
                        foreach (var worker in workers)
                        {
                            await worker.StopAsync(_cancellationTokenSource.Token);
                        }
                    }
                    catch (Exception ex)
                    {
                        Log.Error(ex, $"{ex.Message}");
                    }
                    break;
            }
        }

Please note the @davidfowl suggested that this is not a supported feature (https://github.com/dotnet/extensions/issues/3218#issuecomment-623280990) but I haven't experienced any problem with this approach and believe this should also be a supported use case.

shreesha
  • 1,811
  • 2
  • 21
  • 30