Skip to content

BackgroundService.StopAsync never completes when called from a WPF project from Application.Exit event or Application.OnExit override #42993

Closed

Description

@codingdna2

Description

BackgroundService StopAsync never completes when called from a WPF project from Application.Exit event or Application.OnExit override. Not sure this is the right repo to open the issue so be patient with me.

To Reproduce

Steps to reproduce the behavior (sample project)

Relevant parts:

public partial class App : Application
{
    private IHost _host;

    public static IHostBuilder CreateHostBuilder() => new HostBuilder().ConfigureAppConfiguration((context, configurationBuilder) =>
    {
        configurationBuilder.AddEnvironmentVariables();
        configurationBuilder.SetBasePath(context.HostingEnvironment.ContentRootPath);
    });

    public App()
    {
        _host = CreateHostBuilder().ConfigureServices((context, services) =>
        {
            services.AddHostedService<MyService>();
            services.AddSingleton<MainWindow>();
        }).Build();
    }

    private async void OnApplicationExit(object sender, ExitEventArgs e)
    {
        await _host.StopAsync();

        _host.Dispose();

        _host = null;
    }

    private async void OnApplicationStartup(object sender, StartupEventArgs e)
    {
        await _host.StartAsync();

        var mainWindow = _host.Services.GetService<MainWindow>();
        mainWindow.Show();
    }
}
public class MyService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(10, stoppingToken);
        }
    }
}

Expected behavior

The StopAsync should complete or timeout.

Additional context

I think a deadlock occurs but I've no idea how to prevent it. I've tried to debug the BackgroundService class and the offending part is the Task.WhenAny included in StopAsync. I've also tried to wrap the call to StopAsync in a Task but the effect is the same.

Any suggestion is appreciated.

Activity

  1. OAguinagalde commented on Jun 25, 2020

    @OAguinagalde

    I found myself in the same situation. My use is pretty much the same:

        public partial class App : Application
        {
            private IHost _host;
            public App()
            {
                _host = CreateHostBuilder().Build();
            }
            private async void Application_Startup(object sender, StartupEventArgs e)
            {
                await _host.StartAsync();
                _host.Services.GetService<MainWindow>().Show();
            }
            private async void Application_Exit(object sender, ExitEventArgs e)
            {
                using (_host)
                {
                    _host.Services.GetService<MainWindow>().Close();
                    await _host.StopAsync(TimeSpan.FromSeconds(5));
                }
            }
        }

    After calling Close() on the MainWindow my method Application_Exit is called, and I get this in my logs:

    Information: Microsoft.Hosting.Lifetime[0]: Waiting for the host to be disposed. Ensure all 'IHost' instances are wrapped in 'using' blocks.

    Maybe there is a "proper way" of using Microsoft.Extensions and IHost with WPF but I have no idea how to make them both work properly together.

    When putting the _host in a using statement in all of the methods: the constructor App() , Application_Startup and Application_Exit, then the problem doesn't happen and the application closes properly, but of course, disposing the IHost right after constructing is definetely not the way to go haha.

    Any insights on how to make Microsoft.Extensions and WPF work together nicely?

  2. OAguinagalde commented on Jul 2, 2020

    @OAguinagalde

    I got some help in the C# Discord, here's a workaround:

    private async void Application_Exit(object sender, ExitEventArgs e)
    {
        _host.Services.GetService<MainWindow>().Close();
        var stopTask = _host.StopAsync(TimeSpan.FromSeconds(5));
        _host.Dispose();
        await stopTask;
    }

    For some reason, the StopAsync never actually exits. So, while not a good solution, it works. Still wondering what the proper way would be.

  3. codingdna2 commented on Jul 3, 2020

    @codingdna2
    Author

    Hi, thanks for the workaround. While the workaround works most of the times, it sometimes causes an ObjectDisposedException in my program.

  4. OAguinagalde commented on Jul 3, 2020

    @OAguinagalde

    Yeah it's not a good solution, but can't really find any help with how to approach this situation. As for the exception, a good try/catch and that would do it haha.

  5. BrennanConroy commented on Sep 23, 2020

    @BrennanConroy
    Member

    This looks similar to #35990

    Another workaround is to register a different IHostLifetime that doesn't block.

  6. codingdna2 commented on Sep 24, 2020

    @codingdna2
    Author

    I've tried to implement IHostLifetime but as the other solution, the IHostedService(s) are not shutdown gracefully. In fact, IHostLifetime.StopAsync is never called, as the IHostedService.StopAsync never completes.

    EDIT: Updated sample project with all the relevant parts (IHostLifetime and IHostedService implementations)

    Here's my attempt:

    public class WPFAppLifetime : IHostLifetime, IDisposable
    {
        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    
        public Task WaitForStartAsync(CancellationToken cancellationToken)
        {
            AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
    
            return Task.CompletedTask;
        }
    
        private void OnProcessExit(object sender, EventArgs e)
        {
            Environment.ExitCode = 0;
        }
    
        public void Dispose()
        {
            AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
        }
    }
    public static class WPFLifetimeHostExtensions
    {
        public static IHostBuilder UseWPFAppLifetime(this IHostBuilder hostBuilder)
        {
            return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton<IHostLifetime, WPFAppLifetime>());
        }
    }
    _host = CreateHostBuilder().ConfigureServices((context, services) =>
    {
        services.AddHostedService<MyService>();
        services.AddSingleton<MainWindow>();
    }).UseWPFAppLifetime().Build();
  7. added and removed
    untriagedNew issue has not been triaged by the area owner
    on Oct 8, 2020
  8. added this to the 6.0.0 milestone on Oct 8, 2020
  9. mayorovp commented on Oct 9, 2020

    @mayorovp
    Contributor

    Try this:

        private void OnApplicationExit(object sender, ExitEventArgs e) // this method cannot be async, it MUST wait!
        {
            Task.Run(() => _host.StopAsync()).Wait();
    
            _host.Dispose();
    
            _host = null;
        }

    Or something like this:

        private void OnApplicationExit(object sender, ExitEventArgs e) // this method cannot be async, it MUST wait!
        {
            var frame = new DispatcherFrame(false);
            
            Task.Run(() => {
                try {
                     await _host.StopAsync();
                }
                finally {
                     frame.Continue = false;
                }
            });
    
            Dispatcher.PushFrame(frame);
    
            _host.Dispose();
    
            _host = null;
        }
  10. codingdna2 commented on Oct 12, 2020

    @codingdna2
    Author

    Test results, suggested solution 1:
    Changed slightly adding async: Task.Run(async () => await _host.StopAsync()).Wait();
    but this one ends with an AggregateException 'Remove root: handle=10, parent=01s.WpfTap.Utility.)' containing a TaskCanceledException which (I think) referes to the actual task.

    Test results, suggested solution 2:
    Changed slightly adding async. This one appears to be working as expected! Thanks!
    Updated sample repo with this fix.

    I leave to owners decision to close issue or not.

  11. eerhardt commented on Jul 22, 2021

    @eerhardt
    Member

    With #56057 we no longer block on ProcessExit in the ConsoleLifetime on net6.0. So this issue is now fixed.

    However, the correct fix here is to not use ConsoleLifetime in a WPF app, since it is not a "console app". So implementing your own WpfLifetime is the right idea.

  12. codingdna2 commented on Jul 22, 2021

    @codingdna2
    Author

    Thanks @eerhardt, but I should clarify the issue here was not the ConsoleLifetime (in fact implementing WpfAppLifetime was a test I've commented above) but the usage of Tasks from the OnApplicationExit event.

  13. ghost locked as resolved and limited conversation to collaborators on Aug 22, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions