I’ve written about dependency injection in WinUI many times. Now we must go deeper.
The other day I was working on my desktop app. I had IServiceProvider – DI works fine. But then I thought: what about paths? Where do I save settings? What about background services? What about single instance? What about telling Windows who my app even is?
The default .NET host is made for web apps. Kestrel, request pipelines, all that stuff. I don’t need any of that. I just want my desktop app to start, show a window, run some background tasks, and shut down gracefully when the user closes the window.
So I built my own host. It sounds like a big deal. It’s actually not. Let me show you what I ended up with.
This code is from my new project – Nochein Toolkit. It’s a development toolkit for building modern Windows apps with WinUI.
The project is still under work. The API is very unstable. Things will change. Break. Get fixed. That’s just how it goes right now.
This is actually the first time I’m mentioning this project anywhere. So, hello world, I guess?
The idea
IServiceProvider is fine for libraries. You ask for a service, you get a service. Good.
But an application needs more. It needs to know where to put logs. It needs to know if it’s the only instance running. It needs to start background services before the UI shows up. It needs to stop everything in the right order when the user closes the window.
So a host is basically a wrapper around IServiceProvider that adds lifecycle management. You start it. It runs. You stop it. Everything cleans up after itself.
I built ApplicationHostBuilder for this. You chain a few methods, call BuildAsync, and get an IApplicationHost. Then you call StartAsync to fire up background services, and RunApplicationAsync to show the WinUI window.
Here’s what it looks like in the demo:
await using var host = await new ApplicationHostBuilder()
.WithIdentity("com.nochein.demo")
.WithSingleInstance()
.Configure(ConfigureServices)
.Configure(ConfigureSettings)
.BuildAsync(CancellationToken.None);
await host.RunApplicationAsync();
Three lines to configure. One line to run. I like it when things are simple.
Why interfaces and internal classes everywhere?
You’ll notice a bunch of interfaces – IApplicationHost, IApplicationPaths, IApplicationEnvironment. And then internal implementations like ApplicationHost, ApplicationPaths, ApplicationEnvironment.
The reason is boring but important: testing.
I can mock IApplicationEnvironment in my unit tests and pretend the system theme changed. I don’t need to actually call Windows APIs. That would be slow and unreliable.
The internal classes can change. I can add parameters to constructors. I can change how files are saved. The public interfaces stay stable. This is not a new idea – IHostedService and ILogger work the same way.
AppUserModelId and single instance
Without AppUserModelId, Windows doesn’t know who you are. Taskbar grouping breaks. Jump lists don’t work. Notifications act weird.
The rule is simple: use reverse DNS. Like com.yourcompany.yourapp.
.WithIdentity("com.nochein.demo")
That’s it. The host calls SetCurrentProcessExplicitAppUserModelID for you.
Single instance is also handled. When you add .WithSingleInstance(), the first app creates a named pipe. The second app connects to that pipe, sends its command-line arguments, and exits. The first app wakes up, brings its window to the front, and processes the arguments.
I spent way too much time debugging named pipes once. Now it just works.
Paths – where does all the crap go?
Every app needs to save stuff. Settings, logs, temp files.
IApplicationPaths gives you four properties: UserData, Logs, Temp, and Executable.
You choose the behavior:
- Call
.WithSystemPaths()andUserDatagoes to%APPDATA%\Company\AppName. Good for installed apps. - Don’t call it, and everything stays next to your EXE in
data,logs,tempfolders. Good for portable apps.
I never hardcode paths anymore. Just ask for IApplicationPaths in my services and move on.
Background services
Look, everyone knows how to do this:
Task.Run(() => DoSomething());
That’s not what we’re talking about.
Background services – IHostedService and BackgroundService – are for things that should live as long as your app lives. They start when the app starts. They stop when the app stops. They get a cancellation token when it’s time to clean up.
The single instance pipe listener from the demo is a perfect example:
internal sealed class InstancePipeReceiverService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (stoppingToken.IsCancellationRequested is false)
{
using var pipeServer = new NamedPipeServerStream(...);
await pipeServer.WaitForConnectionAsync(stoppingToken);
// handle the connection
}
}
}
This thing sits there for hours. Days maybe. It waits. It does almost nothing. Then one day a second instance launches, connects to the pipe, and the service wakes up.
This is not about performance. This is about lifetime management.
When the user closes the main window, the host calls StopAsync. The stoppingToken cancels. The service exits its loop. The pipe closes. Everything cleans up.
Try doing that with Task.Run. You’ll end up with a fire-and-forget task that keeps running after your app exits. Or worse – you’ll forget to handle shutdown and get weird errors.
So yeah. Background services are for background lifetimes, not background work.
Settings – because existing options are read-only
Here’s something interesting about the default .NET configuration system. It’s read-only by design. You load appsettings.json at startup. You read values. That’s it. They never change while the app runs.
Why? Because it comes from the web world. In a web app, settings are for the application, not the user. The user doesn’t change the database connection string. The admin changes it, and the app restarts.
Desktop apps are different. Users change things. Themes. Preferred folders. Behavior toggles. These changes need to persist. And the UI should react immediately.
So I built my own settings system. Let me show you what a real setting looks like – this is CommonSettings from the demo:
public class CommonSettings : ISettings, IDisposable
{
public CommonSettings()
{
_windowThemeSubject = new BehaviorSubject<WindowThemeSetting>(WindowThemeSetting.System);
WindowTheme = _windowThemeSubject.DistinctUntilChanged().AsObservable();
_observable = WindowTheme
.Select(windowTheme => new JsonObject
{
["windowTheme"] = windowTheme switch
{
WindowThemeSetting.System => "System",
WindowThemeSetting.Light => "Light",
WindowThemeSetting.Dark => "Dark",
_ => "System"
}
})
.Publish().RefCount();
}
private readonly BehaviorSubject<WindowThemeSetting> _windowThemeSubject;
private readonly IObservable<JsonObject> _observable;
public string Key => "common";
public IObservable<WindowThemeSetting> WindowTheme { get; }
public void SetWindowTheme(WindowThemeSetting value)
{
_windowThemeSubject.OnNext(value);
}
public void Load(JsonNode node) { /* parse JSON */ }
public IObservable<JsonNode> Observe() => _observable;
}
See that SetWindowTheme method? Just OnNext. That’s it.
Now here’s how you use it in a ViewModel – this is SettingsViewModel from the demo:
public sealed class SettingsViewModel : ViewModelBase
{
private readonly CompositeDisposable _disposable = [];
private readonly CommonSettings _commonSettings;
public SettingsViewModel(
CommonSettings commonSettings,
IApplicationContext applicationContext,
IWindow window) : base(window)
{
_commonSettings = commonSettings;
_commonSettings.WindowTheme
.ObserveOn(window.Context.UIContext)
.Subscribe(windowTheme => WindowTheme = windowTheme)
.DisposeWith(_disposable);
}
public WindowThemeSetting WindowTheme
{
get;
set
{
if (SetProperty(ref field, value))
{
_commonSettings.SetWindowTheme(value);
}
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_disposable.Dispose();
}
base.Dispose(disposing);
}
}
Two things to remember when using reactive settings:
1. Observe on the UI thread. Settings changes can come from background threads (loading, auto-saving). You can’t update UI bindings from a background thread. ObserveOn(window.Context.UIContext) switches to the right thread.
2. Dispose your subscriptions. CompositeDisposable collects everything via .DisposeWith(_disposable). Then _disposable.Dispose() in Dispose cleans up. No memory leaks.
Register the setting with the builder:
private static void ConfigureSettings(ISettingsBuilder builder)
{
builder.Add<CommonSettings>();
}
The JSON file ends up in UserData/application.config:
{
"common": {
"windowTheme": "Dark"
}
}
That’s it. No save button. No manual loading. Just change a value and the system handles the rest.
Why not just use the existing stuff?
Good question. The .NET team already made Microsoft.Extensions.Hosting. It works. Why roll your own?
Two reasons. One practical, one philosophical.
Practical: The default host is made for web apps. It assumes you want Kestrel, console lifetime, maybe a web server. For a WinUI desktop app, you end up fighting it. You need STA threads. You need Application.Start(). You need to hook into DispatcherQueue.FrameworkShutdownStarting. The generic host doesn’t know any of this.
Could you make it work? Sure. But you’d write the same amount of code, plus a bunch of hacks to override the default behavior.
Philosophical (the real reason): I wanted to understand how it works.
Reading documentation is fine. But building something from scratch – even a small one – teaches you more. You learn why things are designed the way they are. You discover problems you didn’t know existed.
For example, I never thought about service startup order until I wrote ApplicationHost.StartAsync:
foreach (var service in Services.GetRequiredService<IEnumerable<IHostedService>>())
{
await service.StartAsync(cancellationToken);
_runningServices.Push(service);
}
Simple, right? But then I thought: what if one service fails to start? Should the others keep running? I decided no – if one fails, stop all started services. Maybe that’s wrong. Maybe the real host does something else. But now I know the question exists.
Also, stopping in reverse order (LIFO). Why? Because the last service might depend on the first. If you stop the first service first, the last service might crash. I learned this by almost making the mistake myself.
This is just for fun (mostly)
Look, I’m not saying you should use this in your $BIGCORP enterprise app. Use the real host. It’s tested. It’s supported. It handles edge cases I haven’t even imagined.
But honestly? It’s funny. The .NET team built this massive, flexible hosting system. And here I am, reinventing a tiny wheel for my desktop app. That tiny wheel fits perfectly. No extra spokes. No weird adapters. Just what I need.
Sometimes that’s enough. You learn more by building the small wheel than by reading the manual for the big one.
Happy coding and happy 404 Day – April 4th. If something breaks today, just blame the date.