dotnet run Support for Packaged WinUI Apps

August 13, 2026 · View on GitHub

This document describes the implementation of dotnet run support for packaged WinUI applications using a custom NuGet package.

Overview

The solution enables developers to run packaged WinUI (WinAppSDK) applications using just the .NET CLI:

winapp init
dotnet run

Architecture

Components

  1. Microsoft.Windows.SDK.BuildTools.WinApp (NuGet Package)

    • Contains the WinAppCLI binary in tools/ folder
    • Provides MSBuild targets that hook into dotnet run
    • Automatically detects packaged WinUI apps and handles launch
  2. WinAppCLI

    • Handles debug identity registration
    • Launches packaged apps via Windows Application Activation Manager

How It Works

dotnet run


MSBuild Build Target


_WinAppValidateRunSupport (validates prerequisites; gated by _WinAppRunSupportActive)


_WinAppPrepareRunArguments (overrides RunCommand with CLI path)


Run Target (invokes: winapp run --manifest ...)


WinAppCLI
    ├── Creates loose-layout package
    ├── Registers debug identity
    └── Launches via Application Activation Manager

File Structure

src/
├── winapp-NuGet/                           # BuildTools.WinApp NuGet package
│   ├── Microsoft.Windows.SDK.BuildTools.WinApp.csproj
│   ├── README.md
│   ├── build/
│   │   ├── Microsoft.Windows.SDK.BuildTools.WinApp.props
│   │   └── Microsoft.Windows.SDK.BuildTools.WinApp.targets
│   └── tools/                              # CLI binaries (copied by build script)
│       ├── win-x64/
│       └── win-arm64/

samples/
└── winui-app/                              # Sample WinUI app for testing

MSBuild Integration Details

Properties (Microsoft.Windows.SDK.BuildTools.WinApp.props)

PropertyDefaultDescription
EnableWinAppRunSupporttrueEnable/disable the run support functionality
WinAppManifestPathAuto-detectedPath to the AppxManifest file
WinAppLooseLayoutPath$(OutputPath)AppX\Output directory for loose-layout package
WinAppLaunchArgs(empty)Arguments to pass to the app on launch
WinAppCliPath(in package)Path to the winapp.exe CLI
WinAppRunUseExecutionAliasfalseLaunch via execution alias instead of AUMID. Keeps console I/O in the current terminal. Requires uap5:ExecutionAlias in the manifest. Cannot be combined with WinAppRunNoLaunch.
WinAppRunNoLaunchfalseOnly register package identity without launching the app. Cannot be combined with WinAppRunUseExecutionAlias.
WinAppRunDebugOutputfalseAttach as a debugger to capture OutputDebugString messages and first-chance exceptions. Only one debugger can attach at a time, so Visual Studio or VS Code cannot debug simultaneously. Use WinAppRunNoLaunch instead to attach a different debugger. Cannot be combined with WinAppRunNoLaunch.
WinAppRunDetachfalseReturn immediately after launching instead of waiting for the app to exit. Prints the PID.
WinAppRunUnregisterOnExitfalseUnregister the development package after the app exits. Only removes packages registered in development mode.
WinAppRunCleanfalseRemove the existing package's application data (LocalState, settings) before re-deploying. Application data is preserved by default.
WinAppRunSymbolsfalseDownload symbols from the Microsoft Symbol Server for richer native crash analysis. Only has an effect together with WinAppRunDebugOutput.
WinAppRunExecutable(empty)Executable path relative to the build-output folder. Use to disambiguate when the manifest contains a $targetnametoken$ placeholder and the output folder contains more than one .exe.
WinAppRunArgs(empty)Raw arguments appended to the winapp run command line, for options that have no dedicated property. See Escape hatch.

Mutually exclusive settings

WinAppRunNoLaunch and WinAppRunDetach each describe a different launch behavior — one never starts the app, the other starts it and stops tracking it — so neither can be combined with the properties that need a tracked, running process, nor with each other:

PropertyCannot be combined with
WinAppRunNoLaunchWinAppRunDetach, WinAppRunUseExecutionAlias, WinAppRunDebugOutput, WinAppRunUnregisterOnExit
WinAppRunDetachWinAppRunNoLaunch, WinAppRunUseExecutionAlias, WinAppRunDebugOutput, WinAppRunUnregisterOnExit

The CLI rejects a conflicting pair before doing any work, so the run fails immediately:

> dotnet run -p:WinAppRunDetach=true -p:WinAppRunUnregisterOnExit=true
❌ --detach and --unregister-on-exit cannot be used together.

WinAppRunUseExecutionAlias, WinAppRunDebugOutput, and WinAppRunUnregisterOnExit can be combined with each other. WinAppRunClean, WinAppRunSymbols, WinAppRunExecutable, and WinAppLaunchArgs have no restrictions. WinAppRunSymbols is not rejected on its own, but only has an effect together with WinAppRunDebugOutput. WinAppRunArgs adds no restriction of its own, but a switch passed through it is checked like any other, so WinAppRunArgs="--detach" still conflicts with WinAppRunNoLaunch.

Escape hatch: WinAppRunArgs

Every option winapp run accepts in folder mode has a dedicated property above. WinAppRunArgs exists for the rest — the global options such as --verbose, and any option added to the CLI before a property is wired up for it:

dotnet run -p:WinAppRunArgs="--verbose"

It is appended after every property-derived switch, the same position AdditionalOptions occupies in other toolsets:

run "<output>" --manifest "<manifest>" --detach --caller nuget-package --verbose
                                       ^ from WinAppRunDetach          ^ from WinAppRunArgs

Use it for options that have no property rather than to override one. Repeating a boolean switch is harmless, but winapp rejects a scalar option supplied twice instead of letting the later value win:

> dotnet run -p:WinAppRunExecutable=a.exe -p:WinAppRunArgs="--executable b.exe"
Option '--executable' expects a single argument but 2 were provided.

Targets (Microsoft.Windows.SDK.BuildTools.WinApp.targets)

TargetDescription
_WinAppValidateRunSupportValidates prerequisites (CLI exists, manifest exists)
_WinAppBuildRunArgsBuilds CLI command arguments (shared by run targets)
_WinAppPrepareRunArgumentsOverrides RunCommand to use CLI
RunPackagedAppDirect target to run packaged app
WinAppRunSupportInfoDiagnostic target showing all properties

Detection Logic

The package only activates when all of the following are true (gated by the internal _WinAppRunSupportActive property):

  1. EnableWinAppRunSupport is true (the default). Set it to false to explicitly disable.
  2. WindowsPackageType is not set to None (absence of the property means packaged).
  3. OutputType is not Library — both Exe (packaged console apps via execution alias) and WinExe (WinUI apps) are supported.
  4. The target platform identifier is windows (derived from $(TargetPlatformIdentifier) if set, else from $(TargetFramework)). In multi-targeted projects (e.g. MAUI net*-android;net*-ios;net*-windows10.0.19041.0), the targets are inert for non-Windows TFMs.
  5. WinAppManifestPath resolves to an existing file. The targets auto-detect the manifest by checking the output directory first ($(OutputPath)AppxManifest.xml, $(OutputPath)Package.appxmanifest, $(OutputPath)appxmanifest.xml) and then the project directory (AppxManifest.xml, Package.appxmanifest, appxmanifest.xml); a consumer-supplied WinAppManifestPath is honored as-is. Output-directory paths are accepted because frameworks like MAUI generate the manifest at build time into $(OutputPath) from platform / msbuild props; without that the gate could never activate for transitive MAUI head apps.

This gating ensures the package is safe to consume transitively (e.g. when re-exported by a library): unrelated projects (libraries, test projects, console apps without manifests, non-Windows TFMs) see no winapp activity and no impact on dotnet run.

Build Scripts

package-nuget.ps1

Creates both NuGet packages:

.\scripts\package-nuget.ps1                    # Prerelease version
.\scripts\package-nuget.ps1 -Version 1.0.0 -Stable  # Stable version

Integration with build-cli.ps1

The main build script now includes NuGet packaging:

.\scripts\build-cli.ps1                        # Full build including NuGet
.\scripts\build-cli.ps1 -SkipNuGet             # Skip NuGet packages

Usage

Argument routing

dotnet run behaves the same way whether or not a project references this package: everything you write after dotnet run is passed to your application.

dotnet run --devtools          # your app receives --devtools
dotnet run -- --devtools       # identical
dotnet run --detach            # your app receives --detach

A standalone -- is optional for arguments that do not collide with a dotnet run option: the .NET SDK consumes the separator while parsing its own command line and never re-emits it, so dotnet run --devtools and dotnet run -- --devtools reach winapp as exactly the same token list. (Mechanically, the targets end RunArguments with a separator, so every argument the SDK appends lands in winapp's passthrough region.)

Use -- when your application takes a flag that dotnet run itself defines — --configuration, --framework, --project, --no-build, -c, -f, -r, -v, and friends. Without it the SDK claims the token and your app never sees it:

dotnet run --configuration Release      # the SDK builds Release; your app gets nothing
dotnet run -- --configuration Release   # your app receives --configuration Release

Configure the launcher itself with the WinAppRun* MSBuild properties, which MSBuild consumes:

dotnet run -p:WinAppRunDetach=true --devtools

Here -p:WinAppRunDetach=true detaches the launcher and --devtools goes to your app.

Important

This is a breaking change introduced in this release. Options written directly after dotnet run used to configure WinApp, so dotnet run --detach detached the launcher; now it reaches your application instead. Move those to the matching property (-p:WinAppRunDetach=true). When winapp sees a forwarded argument that matches one of its own options, it prints the replacement:

ℹ '--detach' was passed to your application, not to winapp.
  To configure winapp, use -p:WinAppRunDetach=true instead.

Customization

Disable run support for a project:

<PropertyGroup>
  <EnableWinAppRunSupport>false</EnableWinAppRunSupport>
</PropertyGroup>

Specify manifest path:

<PropertyGroup>
  <WinAppManifestPath>$(MSBuildProjectDirectory)\custom\Package.appxmanifest</WinAppManifestPath>
</PropertyGroup>

Pass launch arguments:

<PropertyGroup>
  <WinAppLaunchArgs>--debug --verbose</WinAppLaunchArgs>
</PropertyGroup>

Launch via execution alias (for console apps):

<PropertyGroup>
  <WinAppRunUseExecutionAlias>true</WinAppRunUseExecutionAlias>
</PropertyGroup>

Register identity without launching:

<PropertyGroup>
  <WinAppRunNoLaunch>true</WinAppRunNoLaunch>
</PropertyGroup>

Capture OutputDebugString messages and first-chance exceptions:

<PropertyGroup>
  <WinAppRunDebugOutput>true</WinAppRunDebugOutput>
</PropertyGroup>

Production Blockers

1. CLI AOT Build Issues (BLOCKING)

The CLI currently has NativeAOT compilation errors related to Newtonsoft.Json and NuGet.Protocol. These must be resolved before the NuGet package can include the CLI binaries.

Error summary:

  • 146 trim/AOT analysis errors
  • Related to reflection-heavy code in Newtonsoft.Json
  • Related to dynamic code generation in NuGet.Protocol

Resolution:

2. Developer Mode Requirement

Running packaged apps requires Developer Mode enabled on Windows. The solution should:

  • Detect when Developer Mode is disabled
  • Provide clear error messages
  • Consider documenting this requirement prominently

3. First-run Experience

On first dotnet run, the CLI needs to:

  • Download Windows SDK Build Tools (if not cached)
  • This can take time on slow connections

Consider pre-caching or documenting this.

4. Platform Detection

The current implementation defaults to x64. For ARM64 machines, the targets correctly detect architecture, but the default Platform may need adjustment.

Testing

Local Testing (without published NuGet)

The sample project imports the MSBuild targets directly:

<Import Project="..\..\src\winapp-NuGet\build\Microsoft.Windows.SDK.BuildTools.WinApp.props" />
<Import Project="..\..\src\winapp-NuGet\build\Microsoft.Windows.SDK.BuildTools.WinApp.targets" />

Diagnostic Commands

# Show MSBuild property values
dotnet msbuild -t:WinAppRunSupportInfo

# Verbose build output
dotnet run -v:detailed

Future Enhancements

  1. Hot Reload Support: Integrate with dotnet watch for live reloading
  2. Debug Attachment: Return process ID for debugger attachment in IDEs
  3. Unpackaged Mode: Auto-detect and use unpackaged mode when appropriate
  4. Multiple Apps: Support projects with multiple Application entries in manifest