Skip to content

Instantly share code, notes, and snippets.

@mattleibow
Last active March 23, 2026 17:58
Show Gist options
  • Select an option

  • Save mattleibow/74e29956ca3e6482884626c2d223e05d to your computer and use it in GitHub Desktop.

Select an option

Save mattleibow/74e29956ca3e6482884626c2d223e05d to your computer and use it in GitHub Desktop.
Plan: Make dotnet run work for MSIX-packaged MAUI apps

Plan: Make dotnet run Work for MSIX-Packaged MAUI Apps

Problem Statement

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:

  1. DeployToDevice — deploy/register step (runs even with --no-build)
  2. ComputeRunArguments — sets $(RunCommand) / $(RunArguments) for launch
  3. ComputeAvailableDevices — 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.

Approach

Create two deliverables following the Android pattern:

  1. MSBuild targets — Wire up DeployToDevice and ComputeRunArguments for MSIX
  2. A .NET run tool (Microsoft.Maui.Windows.Run) — Register, activate, monitor, and terminate the MSIX app

Architecture Diagram

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

Detailed Design

Part 1: The Run Tool — Microsoft.Maui.Windows.Run

A .NET console application shipped in the MAUI NuGet package under tools/.

Subcommands

register — Register MSIX package from loose layout

dotnet exec Microsoft.Maui.Windows.Run.dll register
    --manifest <path-to-AppxManifest.xml>

Implementation:

  • Parse AppxManifest.xml to extract Identity/@Name and Identity/@Publisher
  • Call Windows.Management.Deployment.PackageManager.RegisterPackageByUriAsync() with DevelopmentMode = true
    • URI = file path to AppxManifest.xml
    • This is the equivalent of Add-AppxPackage -Register <path>
  • On success, query PackageManager.FindPackagesForUser("") to get PackageFamilyName
  • 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 IApplicationActivationManager COM object (CLSID 45BA127D-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 just Process.Kill()
  • Wait for process to exit
  • Return app's exit code

COM Interop Declarations Needed

[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 { }

Project Structure

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

Key Dependencies

  • Windows.Management.Deployment.PackageManager (WinRT API, available via TFM targeting)
  • COM interop for IApplicationActivationManager (manual declarations)
  • System.Xml.Linq for manifest parsing
  • No external NuGet packages needed

Part 2: MSBuild Targets

New File: Windows Run Targets

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 &quot;$(_MauiWindowsRunToolPath)&quot; register --manifest &quot;$(_MsixAppxManifestPath)&quot;"
          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 &quot;$(_MauiWindowsRunToolPath)&quot; run --aumid &quot;$(_MsixAppUserModelId)&quot;</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="&lt;Namespace Prefix='def' Uri='http://schemas.microsoft.com/appx/manifest/foundation/windows10' /&gt;">
      <Output TaskParameter="Result" ItemName="_MsixPackageName" />
    </XmlPeek>

    <XmlPeek XmlInputPath="$(_MsixAppxManifestPath)"
             Query="/def:Package/def:Identity/@Publisher"
             Namespaces="&lt;Namespace Prefix='def' Uri='http://schemas.microsoft.com/appx/manifest/foundation/windows10' /&gt;">
      <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:

  1. Option A (Simpler): The run subcommand takes --manifest instead of --aumid, reads the manifest, registers if needed, computes AUMID internally, then activates.
  2. 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.

Revised Simpler Design (Option A)

<Target Name="DeployToDevice"
    Condition="'$(WindowsPackageType)' == 'MSIX'"
    DependsOnTargets="_ComputeWindowsMsixRunProperties">
  <Exec Command="dotnet exec &quot;$(_MauiWindowsRunToolPath)&quot; register --manifest &quot;$(_MsixAppxManifestPath)&quot;" />
</Target>

<Target Name="_WindowsMsixComputeRunArguments"
    BeforeTargets="ComputeRunArguments"
    Condition="'$(WindowsPackageType)' == 'MSIX'"
    DependsOnTargets="_ComputeWindowsMsixRunProperties">
  <PropertyGroup>
    <RunCommand>dotnet</RunCommand>
    <RunArguments>exec &quot;$(_MauiWindowsRunToolPath)&quot; run --manifest &quot;$(_MsixAppxManifestPath)&quot;</RunArguments>
    <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
  </PropertyGroup>
</Target>

The run subcommand will:

  1. Parse manifest → get Identity Name, Publisher, App Id
  2. Register package (idempotent — re-registers if already registered)
  3. Query PackageManager → get PackageFamilyName
  4. Construct AUMID = {PackageFamilyName}!{AppId}
  5. Activate via COM → get processId
  6. Wait for exit / handle Ctrl+C

Import Chain

The new targets file needs to be imported. Add to existing Microsoft.Maui.Sdk.Windows.targets:

<Import Project="Microsoft.Maui.Windows.Run.targets" />

Part 3: Packaging the Tool

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.


Implementation Status

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

Key Corrections from Plan

  • Used PackageManager.RegisterPackageAsync() (NOT AddPackageByUriAsync which is for .appx files)
  • .buildtasks/ directory needs to be populated by rebuilding Controls.Build.Tasks.csproj

Todos

1. Create the Microsoft.Maui.Windows.Run tool project

  • 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 register command using PackageManager.RegisterPackageByUriAsync(DevelopmentMode)
  • Implement run command: register → compute AUMID → activate → wait → Ctrl+C handling
  • Add to solution

2. Create MSBuild targets for MSIX run

  • Create Microsoft.Maui.Windows.Run.targets in the build tasks nuget directory
  • Implement DeployToDevice target (calls tool's register)
  • Implement _WindowsMsixComputeRunArguments (BeforeTargets ComputeRunArguments, sets RunCommand/RunArguments)
  • Implement _ComputeWindowsMsixRunProperties helper target
  • Import from existing Microsoft.Maui.Sdk.Windows.targets
  • Condition everything on '$(WindowsPackageType)' == 'MSIX'

3. Wire up NuGet packaging

  • Ensure Microsoft.Maui.Windows.Run.dll + dependencies are packed into the NuGet under tools/
  • Add build/pack targets to include the tool output
  • Verify the tool path resolves correctly from the targets

4. Test end-to-end with Sandbox app

  • Set WindowsPackageType=MSIX on Sandbox
  • Run dotnet run -f net10.0-windows10.0.19041.0
  • Verify: registration succeeds, app launches, Ctrl+C terminates, exit code propagated

Decisions Made

  1. 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.

  2. Project location: src/WindowsRunTool/ — new top-level directory, clean separation from the netstandard2.0 build tasks.

  3. Package cleanup: Leave registered on exit (like VS) — faster re-runs, no unregister on Ctrl+C.

  4. Sparse packages: Include if easy, skip if complex. Check if the same RegisterPackageByUriAsync(DevelopmentMode) + ActivateApplication flow works for Sparse.

  5. NuGet packaging: Deferred — prototype with hardcoded/local path for testing. Full NuGet tools/ integration comes later.

  6. Console output: Out of scope for v1. No stdout/logcat equivalent for MSIX apps.

Remaining Risks

  • 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=true bypasses 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment