dotnet run fails for MAUI apps with WindowsPackageType=MSIX. The build succeeds, but the SDK launches the EXE directly (unpackaged), causing REGDB_E_CLASSNOTREG because the Windows App SDK auto-initializer needs a packaged identity context. Visual Studio handles this via internal AppxLayoutManager.RegisterPackage() + IApplicationActivationManager.ActivateApplication() COM calls, but the CLI has no equivalent.
The .NET SDK now has an extensibility spec (dotnet-run-for-maui) that defines three MSBuild hook points:
DeployToDevice— deploy/register step (runs even with--no-build)ComputeRunArguments— sets$(RunCommand)/$(RunArguments)for launchComputeAvailableDevices— device selection (not needed for Windows local)
Android already implements all three in Microsoft.Android.Sdk.Application.targets, using a helper tool Microsoft.Android.Run.dll for logcat streaming + Ctrl+C handling.
Create two deliverables following the Android pattern:
- MSBuild targets — Wire up
DeployToDeviceandComputeRunArgumentsfor MSIX - A .NET run tool (
Microsoft.Maui.Windows.Run) — Register, activate, monitor, and terminate the MSIX app
dotnet run -f net10.0-windows10.0.19041.0
│
├─ Build (existing) → produces AppxManifest.xml + layout in bin/
│
├─ DeployToDevice (NEW target)
│ └─ Exec: dotnet exec Microsoft.Maui.Windows.Run.dll register
│ --manifest <AppxManifest.xml path>
│ → Calls PackageManager.RegisterPackageByUriAsync(DevelopmentMode)
│ → Outputs: PackageFamilyName
│
├─ ComputeRunArguments (NEW target, BeforeTargets)
│ └─ Sets RunCommand = dotnet
│ RunArguments = exec Microsoft.Maui.Windows.Run.dll run
│ --aumid <PackageFamilyName>!App
│
└─ Run (existing SDK target)
└─ Exec: dotnet exec Microsoft.Maui.Windows.Run.dll run --aumid ...
→ IApplicationActivationManager.ActivateApplication(aumid)
→ Waits for process exit
→ Ctrl+C → terminates app via IPackageDebugSettings
A .NET console application shipped in the MAUI NuGet package under tools/.
register — Register MSIX package from loose layout
dotnet exec Microsoft.Maui.Windows.Run.dll register
--manifest <path-to-AppxManifest.xml>
Implementation:
- Parse
AppxManifest.xmlto extractIdentity/@NameandIdentity/@Publisher - Call
Windows.Management.Deployment.PackageManager.RegisterPackageByUriAsync()withDevelopmentMode = true- URI = file path to AppxManifest.xml
- This is the equivalent of
Add-AppxPackage -Register <path>
- On success, query
PackageManager.FindPackagesForUser("")to getPackageFamilyName - Write PackageFamilyName to stdout (for MSBuild to capture)
- Handle already-registered case (re-register / update)
- Handle errors: manifest not found, registration failure, OS version issues
run — Activate and monitor the app
dotnet exec Microsoft.Maui.Windows.Run.dll run
--aumid <PackageFamilyName>!<AppId>
[--package-family <name>] # for process monitoring
[--wait-for-exit true] # default true
Implementation:
- Create
IApplicationActivationManagerCOM object (CLSID45BA127D-10A8-46EA-8AB7-56EA9078943C) - Call
ActivateApplication(aumid, args, ActivateOptions.NoErrorUI, out processId) - Open process handle from
processId - Register Ctrl+C handler:
- On Ctrl+C → call
IPackageDebugSettings.TerminateAllProcesses(packageFullName)or justProcess.Kill()
- On Ctrl+C → call
- Wait for process to exit
- Return app's exit code
[ComImport, Guid("2e941141-7f97-4756-ba1d-9decde894a3d")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IApplicationActivationManager
{
int ActivateApplication(
[MarshalAs(UnmanagedType.LPWStr)] string appUserModelId,
[MarshalAs(UnmanagedType.LPWStr)] string arguments,
uint options,
out uint processId);
// ActivateForFile, ActivateForProtocol not needed
}
[ComImport, Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C")]
class ApplicationActivationManager { }
[ComImport, Guid("B1AEC16F-2383-4852-B0E9-8F0B1DC66B4D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IPackageDebugSettings
{
int EnableDebugging([MarshalAs(UnmanagedType.LPWStr)] string packageFullName,
[MarshalAs(UnmanagedType.LPWStr)] string debuggerCommandLine,
[MarshalAs(UnmanagedType.LPWStr)] string environment);
int DisableDebugging([MarshalAs(UnmanagedType.LPWStr)] string packageFullName);
int Suspend([MarshalAs(UnmanagedType.LPWStr)] string packageFullName);
int Resume([MarshalAs(UnmanagedType.LPWStr)] string packageFullName);
int TerminateAllProcesses([MarshalAs(UnmanagedType.LPWStr)] string packageFullName);
// ... more methods
}
[ComImport, Guid("B1AEC16F-2383-4852-B0E9-8F0B1DC66B4D")]
class PackageDebugSettings { }src/WindowsRunTool/
Microsoft.Maui.Windows.Run/
Microsoft.Maui.Windows.Run.csproj # TFM: net10.0-windows10.0.19041.0, OutputType: Exe
Program.cs # Entry point, argument parsing
Commands/
RegisterCommand.cs # Package registration logic
RunCommand.cs # App activation + monitoring
Interop/
ApplicationActivationManager.cs # COM interop declarations
PackageDebugSettings.cs # COM interop declarations
Manifest/
AppxManifestReader.cs # Parse AppxManifest.xml for identity info
Windows.Management.Deployment.PackageManager(WinRT API, available via TFM targeting)- COM interop for
IApplicationActivationManager(manual declarations) System.Xml.Linqfor manifest parsing- No external NuGet packages needed
Location: src/Controls/src/Build.Tasks/nuget/buildTransitive/net6.0-windows10.0.17763.0/Microsoft.Maui.Windows.Run.targets
<Project>
<PropertyGroup Condition="'$(WindowsPackageType)' == 'MSIX'">
<_MauiWindowsRunToolPath Condition="'$(_MauiWindowsRunToolPath)' == ''">$(MSBuildThisFileDirectory)..\tools\Microsoft.Maui.Windows.Run.dll</_MauiWindowsRunToolPath>
</PropertyGroup>
<!-- ================================================================ -->
<!-- DeployToDevice: Register MSIX package from loose layout -->
<!-- Called by 'dotnet run' deploy step. Runs even with --no-build. -->
<!-- ================================================================ -->
<Target Name="DeployToDevice"
Condition="'$(WindowsPackageType)' == 'MSIX'"
DependsOnTargets="_ComputeWindowsMsixRunProperties">
<Exec Command="dotnet exec "$(_MauiWindowsRunToolPath)" register --manifest "$(_MsixAppxManifestPath)""
ConsoleToMSBuild="true">
<Output TaskParameter="ConsoleOutput" PropertyName="_MsixRegistrationOutput" />
</Exec>
<!-- Parse output to get PackageFamilyName -->
<PropertyGroup>
<_MsixPackageFamilyName>$(_MsixRegistrationOutput)</_MsixPackageFamilyName>
</PropertyGroup>
</Target>
<!-- ================================================================ -->
<!-- ComputeRunArguments: Set RunCommand/RunArguments for MSIX launch -->
<!-- ================================================================ -->
<Target Name="_WindowsMsixComputeRunArguments"
BeforeTargets="ComputeRunArguments"
Condition="'$(WindowsPackageType)' == 'MSIX'"
DependsOnTargets="_ComputeWindowsMsixRunProperties">
<PropertyGroup>
<RunCommand>dotnet</RunCommand>
<RunArguments>exec "$(_MauiWindowsRunToolPath)" run --aumid "$(_MsixAppUserModelId)"</RunArguments>
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
</PropertyGroup>
</Target>
<!-- ================================================================ -->
<!-- Helper: Compute MSIX properties from manifest -->
<!-- ================================================================ -->
<Target Name="_ComputeWindowsMsixRunProperties"
Condition="'$(WindowsPackageType)' == 'MSIX'">
<!-- Path to the generated AppxManifest.xml in the output directory -->
<PropertyGroup>
<_MsixAppxManifestPath Condition="'$(_MsixAppxManifestPath)' == ''">$(TargetDir)AppxManifest.xml</_MsixAppxManifestPath>
<!-- App ID from manifest (default "App" for MAUI projects) -->
<_MsixAppId Condition="'$(_MsixAppId)' == ''">App</_MsixAppId>
</PropertyGroup>
<Error Condition="!Exists('$(_MsixAppxManifestPath)')"
Text="Cannot find AppxManifest.xml at '$(_MsixAppxManifestPath)'. Build the project first." />
<!-- Read package identity from manifest using XmlPeek -->
<XmlPeek XmlInputPath="$(_MsixAppxManifestPath)"
Query="/def:Package/def:Identity/@Name"
Namespaces="<Namespace Prefix='def' Uri='http://schemas.microsoft.com/appx/manifest/foundation/windows10' />">
<Output TaskParameter="Result" ItemName="_MsixPackageName" />
</XmlPeek>
<XmlPeek XmlInputPath="$(_MsixAppxManifestPath)"
Query="/def:Package/def:Identity/@Publisher"
Namespaces="<Namespace Prefix='def' Uri='http://schemas.microsoft.com/appx/manifest/foundation/windows10' />">
<Output TaskParameter="Result" ItemName="_MsixPublisher" />
</XmlPeek>
<!-- The tool will compute PackageFamilyName after registration.
For ComputeRunArguments, we need the AUMID which requires PackageFamilyName.
Strategy: The run tool computes it internally from the manifest. -->
</Target>
</Project>Note on AUMID computation: The AUMID requires PackageFamilyName which includes a publisher hash. Two options:
- Option A (Simpler): The
runsubcommand takes--manifestinstead of--aumid, reads the manifest, registers if needed, computes AUMID internally, then activates. - Option B (Follows Android pattern): Deploy step outputs PackageFamilyName, passed to run step.
Recommendation: Option A is simpler and more robust — the tool handles everything. This also means DeployToDevice and ComputeRunArguments can both just reference the manifest path.
<Target Name="DeployToDevice"
Condition="'$(WindowsPackageType)' == 'MSIX'"
DependsOnTargets="_ComputeWindowsMsixRunProperties">
<Exec Command="dotnet exec "$(_MauiWindowsRunToolPath)" register --manifest "$(_MsixAppxManifestPath)"" />
</Target>
<Target Name="_WindowsMsixComputeRunArguments"
BeforeTargets="ComputeRunArguments"
Condition="'$(WindowsPackageType)' == 'MSIX'"
DependsOnTargets="_ComputeWindowsMsixRunProperties">
<PropertyGroup>
<RunCommand>dotnet</RunCommand>
<RunArguments>exec "$(_MauiWindowsRunToolPath)" run --manifest "$(_MsixAppxManifestPath)"</RunArguments>
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
</PropertyGroup>
</Target>The run subcommand will:
- Parse manifest → get Identity Name, Publisher, App Id
- Register package (idempotent — re-registers if already registered)
- Query PackageManager → get PackageFamilyName
- Construct AUMID =
{PackageFamilyName}!{AppId} - Activate via COM → get processId
- Wait for exit / handle Ctrl+C
The new targets file needs to be imported. Add to existing Microsoft.Maui.Sdk.Windows.targets:
<Import Project="Microsoft.Maui.Windows.Run.targets" />The Microsoft.Maui.Windows.Run.dll needs to be included in the MAUI NuGet package. Following the Android pattern ($(MSBuildThisFileDirectory)..\tools\), it should be placed in the NuGet's tools/ folder.
This requires updating the MAUI NuGet packaging to include the built tool.
All core items ✅ complete. Prototype validated end-to-end:
> dotnet run -f net10.0-windows10.0.19041.0 --no-build
Registering MSIX package: com.microsoft.maui.sandbox v1.0.0.1 (x64)
Package registered successfully.
Package family: com.microsoft.maui.sandbox_mgjqykre3bqqe
Launching: com.microsoft.maui.sandbox_mgjqykre3bqqe!App
App launched with PID: 44044
- Used
PackageManager.RegisterPackageAsync()(NOTAddPackageByUriAsyncwhich is for.appxfiles) .buildtasks/directory needs to be populated by rebuildingControls.Build.Tasks.csproj
- Create project at
src/WindowsRunTool/Microsoft.Maui.Windows.Run/ - Target
net10.0-windows10.0.19041.0 - Implement COM interop for
IApplicationActivationManager - Implement manifest parsing (
AppxManifestReader) - Implement
registercommand usingPackageManager.RegisterPackageByUriAsync(DevelopmentMode) - Implement
runcommand: register → compute AUMID → activate → wait → Ctrl+C handling - Add to solution
- Create
Microsoft.Maui.Windows.Run.targetsin the build tasks nuget directory - Implement
DeployToDevicetarget (calls tool's register) - Implement
_WindowsMsixComputeRunArguments(BeforeTargets ComputeRunArguments, sets RunCommand/RunArguments) - Implement
_ComputeWindowsMsixRunPropertieshelper target - Import from existing
Microsoft.Maui.Sdk.Windows.targets - Condition everything on
'$(WindowsPackageType)' == 'MSIX'
- Ensure
Microsoft.Maui.Windows.Run.dll+ dependencies are packed into the NuGet undertools/ - Add build/pack targets to include the tool output
- Verify the tool path resolves correctly from the targets
- Set
WindowsPackageType=MSIXon Sandbox - Run
dotnet run -f net10.0-windows10.0.19041.0 - Verify: registration succeeds, app launches, Ctrl+C terminates, exit code propagated
-
Developer Mode: Friendly error only — detect if Developer Mode is disabled, print a clear message with instructions ("Enable Developer Mode in Settings > For Developers"). No certificate-based fallback.
-
Project location:
src/WindowsRunTool/— new top-level directory, clean separation from the netstandard2.0 build tasks. -
Package cleanup: Leave registered on exit (like VS) — faster re-runs, no unregister on Ctrl+C.
-
Sparse packages: Include if easy, skip if complex. Check if the same
RegisterPackageByUriAsync(DevelopmentMode)+ActivateApplicationflow works for Sparse. -
NuGet packaging: Deferred — prototype with hardcoded/local path for testing. Full NuGet
tools/integration comes later. -
Console output: Out of scope for v1. No stdout/logcat equivalent for MSIX apps.
- Existing registration conflicts: If the package was previously installed from Store or different location, registration may fail. Need good error messages.
- Certificate trust with DevelopmentMode: Verify that
DevelopmentMode=truebypasses certificate checks entirely (expected behavior, needs confirmation). - Where this ultimately belongs: The spec says "we can either add this logic into the .NET MAUI workload or WindowsAppSDK itself." Starting in MAUI; could migrate later.