Summary
dotnet publish of an unpackaged WinUI app (UseWinUI=true, WindowsPackageType=None) silently omits the app's own <app>.pri, while correctly publishing the Windows App SDK runtime PRIs (Microsoft.UI.pri, Microsoft.UI.Xaml.Controls.pri, …).
The PRI is generated into the build output. It is simply never wired into the publish item list.
Note: an earlier revision of this issue proposed an AfterTargets → BeforeTargets change on AddProjectPriToResolvedFileToPublish. That was wrong — I tested it by patching the targets file directly and it does not fix anything, because that target is never imported in an unpackaged build. Corrected root cause below.
Repro
Minimal blank WinUI 3 csproj — no third-party framework involved:
<OutputType>WinExe</OutputType>
<UseWinUI>true</UseWinUI>
<WindowsPackageType>None</WindowsPackageType>
dotnet publish BlankWinUI3.csproj -c Release -p:Platform=x64 -p:PublishAot=false -o out
| Location |
BlankWinUI3.pri |
build output (bin\x64\Release\net10.0-windows10.0.22621.0) |
present (672 bytes) |
publish output (out) |
absent |
Root cause
The publish wiring exists, but only on the MSIX packaging code path, which an unpackaged project never imports.
Microsoft.Windows.SDK.BuildTools.MSIX.targets gates the packaging chain:
<ShouldImportMsixCommonTargets Condition="'$(ShouldImportMsixCommonTargets)'==''">$(MsixPackageSupport)</ShouldImportMsixCommonTargets>
...
<Import Condition="Exists('$(MsixCommonTargets)') and '$(ShouldImportMsixCommonTargets)' == 'true'" Project="$(MsixCommonTargets)" />
<!-- Import the MRT Core .targets (these will only do anything if the MSIX tooling is disabled) -->
<Import Project="$(MSBuildThisFileDirectory)Microsoft.Windows.SDK.BuildTools.MSIX.MrtCore.targets" ... />
For the repro project, evaluated:
MsixPackageSupport = '' (empty)
ShouldImportMsixCommonTargets = '' (empty -> import condition '== true' is false)
WindowsPackageType = None
So the MSIX chain — Microsoft.Windows.SDK.BuildTools.MSIX.Packaging.targets, and the Microsoft.Windows.SDK.BuildTools.MSIX.Pri.targets it imports at line 4813 — is never imported. Pri.targets is where AddProjectPriToResolvedFileToPublish lives, so that target simply does not exist in the project:
> dotnet build BlankWinUI3.csproj -t:AddProjectPriToResolvedFileToPublish
error MSB4057: The target "AddProjectPriToResolvedFileToPublish" does not exist in the project.
Meanwhile the PRI itself is produced by the MrtCore path, which is imported unconditionally (and, per the comment above, is the path that does the work precisely when MSIX tooling is off). That path has no publish wiring at all:
| Targets file |
ResolvedFileToPublish / CopyToPublishDirectory hits |
...MSIX.MrtCore.targets |
0 |
...MSIX.MrtCore.PriGen.targets |
0 |
...MSIX.MrtCore.PriExpansion.targets |
0 |
...MSIX.MrtCore.Tasks.targets |
0 |
...MSIX.Packaging.targets (MSIX-only) |
10 |
...MSIX.Pri.targets (MSIX-only) |
3 |
In short: the code that publishes the PRI lives only on the MSIX path, while the code that generates the PRI for unpackaged apps lives on the MrtCore path — which has none.
The relevant properties are all correctly populated on the unpackaged path, so nothing else is missing — only the item wiring:
ProjectPriFullPath = ...\bin\x64\Release\net10.0-windows10.0.22621.0\BlankWinUI3.pri
ProjectPriFileName = BlankWinUI3.pri
CopyBuildOutputToPublishDirectory = true
Suggested fix
Wire the app PRI into ResolvedFileToPublish on the MrtCore / unpackaged path (or move/duplicate AddProjectPriToResolvedFileToPublish somewhere imported regardless of ShouldImportMsixCommonTargets). Verified working when injected into an unpackaged build:
<Target Name="AddProjectPriToResolvedFileToPublish"
BeforeTargets="ComputeResolvedFilesToPublishList"
DependsOnTargets="$(GenerateProjectPriFile)">
<ItemGroup>
<ResolvedFileToPublish Include="$(ProjectPriFullPath)"
Condition="Exists('$(ProjectPriFullPath)')">
<RelativePath>$(ProjectPriFileName)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
With this present, BlankWinUI3.pri lands in the publish output.
Impact
Silent at publish time, fatal at runtime for apps that need their PRI. Severity depends on whether the app resolves resources from its own PRI:
- A trivial code-only WinUI app (a
TextBlock, no resource dictionaries) runs fine without it, so this can go unnoticed.
- An app that loads XAML resource dictionaries dies ~10 seconds after launch with
0xC000027B (stowed XAML exception), faulting in Microsoft.UI.Xaml.dll. There is no build or publish diagnostic pointing at the missing file.
Verified bidirectionally on such an app: with the .pri present it launches and renders correctly; delete that one file from the same publish output and it exits 0xC000027B. Nothing else changed.
This also affects .NET 10 file-based apps (dotnet publish app.cs) identically — same missing file, same crash — for both AOT and non-AOT publish.
Workaround for consumers
The target above can be dropped into a Directory.Build.targets (or injected via CustomAfterMicrosoftCommonTargets). It is additive and Exists()-guarded, so it goes inert once this is fixed.
Environment
- .NET SDK 10.0.400
Microsoft.Windows.SDK.BuildTools.MSIX 1.7.251221100, Microsoft.WindowsAppSDK.WinUI 2.1.0
- Windows 11, x64, unpackaged (
WindowsPackageType=None)
Summary
dotnet publishof an unpackaged WinUI app (UseWinUI=true,WindowsPackageType=None) silently omits the app's own<app>.pri, while correctly publishing the Windows App SDK runtime PRIs (Microsoft.UI.pri,Microsoft.UI.Xaml.Controls.pri, …).The PRI is generated into the build output. It is simply never wired into the publish item list.
Repro
Minimal blank WinUI 3 csproj — no third-party framework involved:
BlankWinUI3.pribin\x64\Release\net10.0-windows10.0.22621.0)out)Root cause
The publish wiring exists, but only on the MSIX packaging code path, which an unpackaged project never imports.
Microsoft.Windows.SDK.BuildTools.MSIX.targetsgates the packaging chain:For the repro project, evaluated:
So the MSIX chain —
Microsoft.Windows.SDK.BuildTools.MSIX.Packaging.targets, and theMicrosoft.Windows.SDK.BuildTools.MSIX.Pri.targetsit imports at line 4813 — is never imported.Pri.targetsis whereAddProjectPriToResolvedFileToPublishlives, so that target simply does not exist in the project:Meanwhile the PRI itself is produced by the MrtCore path, which is imported unconditionally (and, per the comment above, is the path that does the work precisely when MSIX tooling is off). That path has no publish wiring at all:
ResolvedFileToPublish/CopyToPublishDirectoryhits...MSIX.MrtCore.targets...MSIX.MrtCore.PriGen.targets...MSIX.MrtCore.PriExpansion.targets...MSIX.MrtCore.Tasks.targets...MSIX.Packaging.targets(MSIX-only)...MSIX.Pri.targets(MSIX-only)In short: the code that publishes the PRI lives only on the MSIX path, while the code that generates the PRI for unpackaged apps lives on the MrtCore path — which has none.
The relevant properties are all correctly populated on the unpackaged path, so nothing else is missing — only the item wiring:
Suggested fix
Wire the app PRI into
ResolvedFileToPublishon the MrtCore / unpackaged path (or move/duplicateAddProjectPriToResolvedFileToPublishsomewhere imported regardless ofShouldImportMsixCommonTargets). Verified working when injected into an unpackaged build:With this present,
BlankWinUI3.prilands in the publish output.Impact
Silent at publish time, fatal at runtime for apps that need their PRI. Severity depends on whether the app resolves resources from its own PRI:
TextBlock, no resource dictionaries) runs fine without it, so this can go unnoticed.0xC000027B(stowed XAML exception), faulting inMicrosoft.UI.Xaml.dll. There is no build or publish diagnostic pointing at the missing file.Verified bidirectionally on such an app: with the
.pripresent it launches and renders correctly; delete that one file from the same publish output and it exits0xC000027B. Nothing else changed.This also affects .NET 10 file-based apps (
dotnet publish app.cs) identically — same missing file, same crash — for both AOT and non-AOT publish.Workaround for consumers
The target above can be dropped into a
Directory.Build.targets(or injected viaCustomAfterMicrosoftCommonTargets). It is additive andExists()-guarded, so it goes inert once this is fixed.Environment
Microsoft.Windows.SDK.BuildTools.MSIX1.7.251221100,Microsoft.WindowsAppSDK.WinUI2.1.0WindowsPackageType=None)