4

I'm writing a WinUI3 (Project Reunion 0.5) application with .NET 5 and would like to use the .NET Generic Host. I'm using the default host with a custom IHostedService:

public App() {
    _host = Host.CreateDefaultBuilder()
        .ConfigureServices((context, services) =>
        {
            services.AddHostedService<MyHostedService>();
        }).Build();
    InitializeComponent();
}

The hosted service performs some asynchronous operations in StopAsync. For demonstration purposes, let's say it delays for 1 second (this code still produces the issue):

public override async Task StopAsync(CancellationToken cancellationToken)
{
    await Task.Delay(1000);
}

I start the host in OnLaunched:

protected override async void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
    await _host.StartAsync();
    m_window = new MainWindow();
    m_window.Activate();
}

I let the default ConsoleLifetime implementation stop the host before the process exits.

The Task returned by my IHostedService.StopAsync implementation completes, but IHost.StopAsync never returns and the process hangs with this message in the output:

Microsoft.Hosting.Lifetime: Information: Application is shutting down...
Microsoft.Hosting.Lifetime: Information: Waiting for the host to be disposed. Ensure all 'IHost' instances are wrapped in 'using' blocks.

If I step through with the debugger, sometimes the IHost.StopAsync method will time out and an exception will be thrown. This never happens outside of the debugger. I have tried explicitly stopping and disposing the host when the MainWindow is closed, but it didn't make any difference.

I thought perhaps the DispatcherQueueSynchronizationContext was being shut down before the host could stop and tasks were not being serviced, but the DispatcherQueue.ShutdownStarting event is never fired.

Any other ideas?

David Brown
  • 35,411
  • 11
  • 83
  • 132
  • Do you experience the same issue using a programming environment with deterministic garbage collection, like C++? – IInspectable Jun 03 '21 at 17:13
  • Where is `App()` defined? Are you using WPF, UWP XAML, or some other XAML framework? – Dai Jun 03 '21 at 17:22
  • You really shouldn't set up your `IHost` inside `App.xaml`'s constructor because it hides the fact that a lot of set-up happens elsewhere in your program which may be contributing towards the problem you're having. Instead you need to define your own `Main` and set up `IHost` **before** WPF, UWP, Jupiter, _whatever-Microsoft-is-calling-their-latest-XAML-environment-this-season_ starts up. – Dai Jun 03 '21 at 17:24
  • @IInspectable C++ is not a "deterministically garbage collected" language: there is no garbage collection in _real_ C++ at all. And even if it was, C++ can't be used to host `IHost` because that requires the CLR. (and compiling C++/CLI does not magically let you use CLR types in "real" C++). – Dai Jun 03 '21 at 17:26
  • @dai You sure you understand what garbage collection is? Cause [everybody thinks about garbage collection the wrong way](https://devblogs.microsoft.com/oldnewthing/20100809-00/?p=13203). – IInspectable Jun 03 '21 at 17:30
  • @IInspectable That entire article by Raymond Chen is only concerned with the .NET/CLR GC and focuses on finalizers - but there are no finalizers in C++ - only destructors - which are invoked deterministically. As C++ does not (normally) let you have objects that are out-of-scope-but-not-yet-destructed then it cannot have garbage waiting to be collected - therefore GC is the wrong term to use to describe C++'s manual memory management - unless I'm missing something? To my knowledge the only "deterministic GC" that exists is ARC, which plenty of C++ libraries will do for you but it's not in-box. – Dai Jun 03 '21 at 17:31
  • @IInspectable Yes. The crux of the article is (to quote) "GC is simulating a computer with an infinite amount of memory." - but C++ does not do that, nor does C++ attempt to do that. Consider that [you can run C++ code on a machine without a heap/free-store](https://embeddedartistry.com/blog/2020/02/17/qa-how-can-i-use-c-on-embedded-projects-when-i-cant-use-the-heap/), which leaves only statics and autos; and referring to autos as "GC" is a gross misrepresentation, imo. – Dai Jun 03 '21 at 17:41
  • @dai Mind to explain what steps you need to take to have a C++ program run indefinitely? I don't have to do anything, because, ya know, **any** practical programming language is garbage collected. Before you bring up some other embedded story to make some point (which I didn't get), make sure to understand that [the null garbage collector is a valid garbage collector](https://devblogs.microsoft.com/oldnewthing/20180228-00/?p=98125). – IInspectable Jun 04 '21 at 07:23

1 Answers1

4

I took @Dai's advice from the comments and investigated running WinUI on a separate thread and running the host on the main thread.

I created an IHostedService to manage the WinUI application:

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.System;
using Microsoft.UI.Xaml;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace MyApp.Hosting
{

public class WinUIHostedService<TApplication> : IHostedService, IDisposable
    where TApplication : Application, new()
{
    private readonly IHostApplicationLifetime HostApplicationLifetime;
    private readonly IServiceProvider ServiceProvider;

    public WinUIHostedService(
        IHostApplicationLifetime hostApplicationLifetime,
        IServiceProvider serviceProvider)
    {
        HostApplicationLifetime = hostApplicationLifetime;
        ServiceProvider = serviceProvider;
    }

    public void Dispose()
    {
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        var thread = new Thread(Main);
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }

    private void Main()
    {
        WinRT.ComWrappersSupport.InitializeComWrappers();
        Application.Start((p) => {
            var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
            SynchronizationContext.SetSynchronizationContext(context);
            new TApplication();
        });
        HostApplicationLifetime.StopApplication();
    }
}

}

I defined DISABLE_XAML_GENERATED_MAIN in the build settings and added my own Main:

public class Program
{
    public static void Main(string[] args)
    {
        Host.CreateDefaultBuilder()
            .ConfigureServices(services =>
            {
                services.AddHostedService<WinUIHostedService<App>>();
            })
            .Build().Run();
    }
}

Voila! The WinUI application still runs fine and the host stops cleanly when the main window closes, even when IHostedService.StopAsync runs asynchronous code.

Note that this code is just the first thing that worked. It could probably be improved and I don't fully understand the Generic Host lifetime semantics.

David Brown
  • 35,411
  • 11
  • 83
  • 132
  • 2
    Thank you for doing this. I really like using the Hosting pipeline and it makes so much sense to follow this pattern. Hopefully there are some official guides or extensions. Since Maui is doing something similar with the Hosting/StartUp pattern – Patrick Magee Jul 07 '21 at 19:28
  • I'm glad I found this. I had been working on creating a WPF application and ran into some frustration with defining some custom grouping when I found the WinUI 3 and the WindowsAppSDK. I wanted to implement dependency injection and it obviously wasn't straightforward to implement until I found this. This bit of code and writeup should be a learn.microsoft.com article. – ThunderEagle Dec 10 '22 at 05:00