The Deployment Nightmare
You have built a robust WinUI 3 application. It runs perfectly in Visual Studio. It runs perfectly when you launch it from your bin folder. You decide to bypass MSIX packaging for a standard "unpackaged" (XCopy or MSI-based) deployment. You copy your release artifacts to a clean client machine, install the Windows App SDK Runtime, and launch the executable.
It crashes immediately. No UI, no error dialog.
Check the Windows Event Viewer, and you likely see a generic .NET Runtime error or an Application Error with exception code 0x80040154 (REGDB_E_CLASSNOTREG) or 0xE0434352.
This is the "Class Not Registered" error, and it is the single most common hurdle when moving from Packaged (MSIX) to Unpackaged WinUI 3 development.
Root Cause Analysis: The Missing Identity
To understand the crash, you must understand how WinUI 3 (Windows App SDK) differs from UWP or WPF.
WinUI 3 is decoupled from the OS. Its binaries live in the Windows App SDK framework package.
- In a Packaged App (MSIX): The
Package.appxmanifestdefines a dependency on the Windows App SDK Framework Package. When Windows launches your app, the OS loader reads this manifest, locates the installed Framework Package, and loads theMicrosoft.UI.Xaml.dll(and others) into your process address space before your entry point runs. - In an Unpackaged App: Your
.exeis a standard Win32 process. The OS loader has no manifest to read. It does not know that your app depends on the Windows App SDK.
When your code hits new Microsoft.UI.Xaml.Application(), it attempts to instantiate a COM object provided by the SDK. Since the SDK DLLs haven't been loaded and registered in the process, the COM activation fails with Class Not Registered.
To fix this, we must manually invoke the Dynamic Dependency API (specifically the Bootstrapper) to locate and load the Windows App SDK runtime before any XAML code executes.
The Fix: Manual Bootstrapping
While the .csproj property <WindowsPackageType>None</WindowsPackageType> attempts to inject a default bootstrapper, it is often fragile in complex build configurations or self-contained deployments.
The most reliable solution for a Principal Engineer is to take control of the entry point. We will disable the auto-generated Main method and write a custom one that handles the Bootstrapper lifecycle explicitly.
Step 1: Update the Project File (.csproj)
First, ensure your project is set to Unpackaged and disable the default generated program entry point.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<UseWinUI>true</UseWinUI>
<Platforms>x86;x64;arm64</Platforms>
<!-- 1. Define Unpackaged Mode -->
<WindowsPackageType>None</WindowsPackageType>
<!-- 2. Prevent VS from generating a hidden Main method -->
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240311000" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.1" />
</ItemGroup>
</Project>
Step 2: Implement the Custom Entry Point
Create a new file named Program.cs. We will use the Microsoft.Windows.ApplicationModel.DynamicDependency.Bootstrap API.
Note: This code assumes you have installed the Windows App SDK Runtime Redistributable on the target machine.
using System;
using System.Runtime.InteropServices;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.Windows.ApplicationModel.DynamicDependency;
namespace WinUIUnpackaged;
public static class Program
{
[STAThread]
static void Main(string[] args)
{
// 1. Initialize the Bootstrapper.
// This locates the best version of the Windows App SDK installed on the
// system and loads it into the process.
// 0x00010005 is the release version (major.minor) of the SDK you are targeting.
// Since we are using the projection, we can often rely on default initialization,
// but explicit initialization is safer for debugging.
// Note: Release 1.5 corresponds to Major 1, Minor 5.
// If Initialize fails, it throws an exception (e.g. Runtime not found).
if (!InitializeWindowsAppSDK())
{
return;
}
try
{
// 2. Start the WinUI XAML Application
Application.Start((p) =>
{
var context = new DispatcherQueueSynchronizationContext(
DispatcherQueue.GetForCurrentThread());
System.Threading.SynchronizationContext.SetSynchronizationContext(context);
// Ensure you have an App.xaml file in your project
new App();
});
}
finally
{
// 3. Clean up the Bootstrapper references on exit
Bootstrap.Shutdown();
}
}
private static bool InitializeWindowsAppSDK()
{
try
{
// Initializes the Bootstrapper to find the Windows App SDK framework package.
// Parameters can be left default to match the NuGet package version linked at build time.
Bootstrap.Initialize();
return true;
}
catch (Exception ex) when (ex is DllNotFoundException || ex is System.IO.FileNotFoundException)
{
// This specifically catches scenarios where the Windows App Runtime
// is missing from the client machine.
ShowErrorDialog("Windows App SDK Runtime is missing. Please install the latest redistributable.");
return false;
}
catch (Exception ex)
{
ShowErrorDialog($"Failed to initialize Windows App SDK: {ex.Message}");
return false;
}
}
// Simple P/Invoke to show a native MessageBox if the framework is missing
// (Since we can't use Xaml Window yet!)
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);
private static void ShowErrorDialog(string message)
{
MessageBox(IntPtr.Zero, message, "Fatal Error", 0x00000010); // MB_ICONHAND
}
}
Technical Breakdown
1. DISABLE_XAML_GENERATED_MAIN
By default, the WinUI build tools generate a file in obj\ that contains a Main method calling Application.Start. If we don't define this constant in the .csproj, the compiler will throw error CS0017 (Program has more than one entry point). We disable the auto-gen version to inject our initialization logic.
2. Bootstrap.Initialize()
This is the critical line. When called, the Bootstrapper:
- Looks up the Windows App SDK Framework packages installed on the user's machine (via the
GetPackagesByPackageFamilyWin32 API). - Selects the best match based on the version compiled into your app (metadata embedded by the NuGet package).
- Injects the path of that package's DLLs into the process's
PATHor usesAddDllDirectoryto ensure subsequentLoadLibrarycalls succeed.
If this line is skipped, new App() calls into Microsoft.UI.Xaml.dll, the OS fails to find the DLL, and the application crashes.
3. Graceful Failure
A distinct advantage of manual bootstrapping is error handling. If the client machine is missing the runtime, the auto-generated entry point simply crashes with a generic KernelBase.dll error.
In the code above, we wrap the initialization in a try/catch. If the runtime is missing, we use a native MessageBox (via P/Invoke user32.dll) to inform the user why the app won't start. You cannot use a WinUI ContentDialog here because the XAML engine hasn't loaded yet.
Conclusion
Unpackaged WinUI 3 offers flexibility for distribution outside the Microsoft Store, but it shifts the responsibility of dependency management to the developer. By taking control of the Main entry point and explicitly initializing the Bootstrapper, you eliminate the "Class Not Registered" startup crash and ensure your application fails gracefully if prerequisites are missing.