One of the unwritten laws in procedural programming is that any function you call will, at one point, end. In the presence of exceptions, this does not mean that the function will return gracefully, but it will end non-the-less.
When this does not happen, something very strange is afoot. For example, C’s
exit() function ends a program right here and now. But this was a WPF application in C#, only using official libraries. Surely there was nothing like that there. And all I was doing was trying to dispose of my SignalR connection on on program shutdown.
I had registered a delegate for “Exit” in my App.xaml’s
<Application>. The SignalR client only implements
IAsyncDisposable, so I made that shutdown function asynchronous using the
async keyword. I
awaited the client’s
DisposeAsync and the program just stopped right there, not getting to any code I wanted to dispose of after that. No exception thrown either. Very weird.
Trying to step into the function with a debugger, I learned that the program exited when the SignalR client’s DisposeAsync was
awaiting something itself. Just exited normally with exit code 0.
At that point, it became painfully obvious what was happening.
async functions do not behave as predictably as normal function. Whenever they are awaiting something, their “tail” is in fact posted to a dispatcher, which resumes the function at that point when the awaited
Task is completed. But since I was already exiting my application, the
Dispatcher was no longer executing newcomers like the remainder of my shutdown sequence.
To fix this, I reversed the order: when a user triggers an application exit, I first clean up my client and then trigger application exit.