Microsoft.Maui.Controls を 9.0.70 から 9.0.81 に上げた直後、ビルドで NU1105「PackageVersion が複数値を持っている」に遭遇しました。再起動や bin/obj の削除では直らないこの症状は、マルチターゲット環境でのバージョン定義の分裂が主因でした。ここでは再現条件、原因の見分け方、確実に解決する手順、今後の予防策まで実務目線で整理します。
ケース概要(発生した状況)
前提:.NET MAUI アプリの .csproj で iOS / Mac Catalyst / Windows を同時にビルドする構成(<TargetFrameworks>)を採用。
操作:NuGet の Microsoft.Maui.Controls を 9.0.70 → 9.0.81 に更新。
直後の症状:ビルド時に下記エラーで停止。
NU1105 Unable to read project information ...
The property PackageVersion was expected to have a single value across all
target frameworks, but instead had the following values: 1.1, 1.0.0
Visual Studio の再起動/bin, obj フォルダー削除/dotnet restore のやり直しでも改善せず。
先に結論(最短で抜けるための選択肢)
- 最短回避:一時的に
<TargetFramework>へ切り替え、ターゲットごとに個別ビルド(CI も分割)。 - 恒久対策:バージョン系のプロパティ(
Version/PackageVersion/ApplicationDisplayVersion/ApplicationVersion等)を単一箇所に集約し、全ターゲットで同一値にする。プラットフォーム固有の値は各 OS のマニフェストに寄せる。
NU1105 とは(要点だけ)
NU1105 は、マルチターゲット プロジェクトの復元・ビルド評価中に、「本来は全ターゲットで同一であるべきプロパティ」に複数の値が混在したときに出ます。代表例が PackageId や PackageVersion。SDK スタイル プロジェクトでは PackageVersion は未指定の場合、通常 Version の値から自動的に補われます。そのため、Version をターゲット別に変えてしまうと、結果として PackageVersion も「複数値」と判定され、NU1105 に至ります。
なぜ MAUI の更新で表面化するのか
MAUI 9.0 系へ上げるタイミングでは、過去バージョンの作法やテンプレートから移行してきた PropertyGroup が複数残っているケースが多く、そこに Version や ApplicationDisplayVersion、ApplicationVersion などが二重三重に定義されがちです。
さらに <TargetFrameworks> で net9.0-ios / net9.0-maccatalyst / net9.0-windows を同時ビルドすると、評価タイミングの違いにより「同じプロパティ名なのにターゲットごとに値が違う」状態が露呈します。更新自体が直接の原因ではなく、古い定義の残骸+マルチターゲットの組み合わせで顕在化したと捉えるのが実務的です。
原因の内訳(深掘り)
| 主な要因 | 詳細 |
|---|---|
| 多重ターゲット & 重複定義 | <TargetFrameworks> で net9.0‑ios; net9.0‑maccatalyst; net9.0‑windows を同時ビルドしつつ、.csproj に Version / ApplicationVersion / ApplicationDisplayVersion 等が 複数の PropertyGroup で定義され、値が 1.0.0 と 1.1 のように分裂。その結果 PackageVersion(既定で Version を参照)がターゲットごとにバラつき、NU1105。 |
| 古い設定の残骸 | .NET 6/7/8 期のテンプレート由来の条件付き PropertyGroup が残留し、$(TargetFramework) 条件で古い値を注入。MAUI 9.0 更新の前後で評価順が変わった/新しい SDK ターゲットが増えたことで衝突。 |
まず確認:プロパティがどこで二重定義されているか
- 詳細ログで確認
dotnet build -v:diag > build.logbuild.log内でProperty reassignmentやPackageVersion/Versionの最終値を検索し、どの.props/.targets/.csprojで上書きされたかを追います。 - バイナリ ログで可視化
dotnet build /bl生成されたmsbuild.binlogを MSBuild Structured Log Viewer で開き、Build → Project Evaluation → Properties のツリーからVersion/PackageVersion/Application*の「設定元」と「結果値」を確認します。 - 前処理出力で競合源を特定
dotnet msbuild -preprocess:preprocessed.xml前処理後のpreprocessed.xmlには、インポート済みの全.props/.targetsが統合されます。ここで同名プロパティの重複定義を洗い出します。
解決策(実用レシピ)
1. ターゲットを 1 つずつビルドする(即効性)
マルチターゲットをいったん解除し、個別にビルド/配布する運用へ切り替えると、NU1105 を即回避できます。
<!-- 変更前(マルチターゲット) -->
<TargetFrameworks>net9.0-ios;net9.0-maccatalyst;net9.0-windows10.0.19041</TargetFrameworks>
<!-- 変更後(例:iOS だけ) -->
<TargetFramework>net9.0-ios</TargetFramework>
CI ではジョブを分け、iOS / MacCatalyst / Windows を別々のワークフローでビルドします。これにより、各ビルドが「単一ターゲット」の前提で評価され、プロパティの単一性が担保されます。
2. バージョン情報を 1 か所に集約(恒久対策の中心)
同名プロパティの多重定義をやめるのが本筋です。プロジェクト直下に集約する例:
<PropertyGroup>
<Version>1.1.0</Version> <!-- <= PackageVersion の既定値 -->
<ApplicationDisplayVersion>1.1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion> <!-- Android / Windows の整数ビルド番号 -->
<GeneratePackageOnBuild>false</GeneratePackageOnBuild> <!-- 必要に応じて -->
</PropertyGroup>
プラットフォームごとに値を変えたい場合は、マニフェスト側に寄せます。
- iOS → Info.plist
- Android → AndroidManifest.xml
- Windows → Package.appxmanifest
どうしても .csproj で条件分けするなら、Version は共通に保ち、ApplicationDisplayVersion や OS 固有の設定のみ条件化します。
3. 使わない PropertyGroup を削除
下記のような条件付きブロックが残っていないか棚卸しし、不要なら削除します。
<!-- 旧 TFM 用の条件付きブロック。残っていると評価順で古い値が勝つことがある -->
<PropertyGroup Condition="'$(TargetFramework)' == 'net6.0-android'">
<ApplicationDisplayVersion>1.0.0</ApplicationDisplayVersion>
</PropertyGroup>
4. NuGet & ワークロードのクリーンアップ
プロパティ衝突を解消した上で、キャッシュやワークロードも整え直します。
dotnet nuget locals all --clear # NuGet キャッシュ削除
dotnet workload update # MAUI 関連ワークロードを最新化
dotnet workload restore # 必要ワークロードの再取得
# その後、bin / obj / .vs を削除 → dotnet restore → ビルド
5. Mac ビルドエージェント接続エラーへの対処
iOS ビルド時に Mac 側のワークロードが古くて失敗することがあります。Xcode 16 対応の MAUI ワークロード 9.0.204 を Mac に導入すると解消した事例があります(環境に合わせて更新)。
マルチターゲットを維持したい場合の指針
依存パッケージの版ずれを可視化・統一
dotnet list package --include-transitive
ターゲット間で版が食い違うものは、明示的に統一します。
<ItemGroup>
<PackageReference Update="Microsoft.Maui.Controls" Version="9.0.81" />
<!-- 依存鎖でズレるものもここで揃える -->
</ItemGroup>
Directory.Build.props / Directory.Packages.props を活用
ソリューション単位で値を固定すると、個々の .csproj の分裂を予防できます。
<!-- Directory.Build.props の例 -->
<Project>
<PropertyGroup>
<Version>1.1.0</Version>
<ApplicationDisplayVersion>1.1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
</PropertyGroup>
</Project>
<!-- Directory.Packages.props の例 -->
<Project>
<ItemGroup>
<PackageVersion Include="Microsoft.Maui.Controls" Version="9.0.81" />
<!-- 他の共通パッケージ版もここで固定 -->
</ItemGroup>
</Project>
ダメな例と良い例(.csproj 抜粋)
NG:同名プロパティをターゲット別にバラバラ定義
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net9.0-ios;net9.0-maccatalyst;net9.0-windows10.0.19041</TargetFrameworks>
</PropertyGroup>
1.1.0
1.1.0
1.0.0
1.0.0
OK:Version は共通、OS 固有はマニフェストへ
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net9.0-ios;net9.0-maccatalyst;net9.0-windows10.0.19041</TargetFrameworks>
<UseMaui>true</UseMaui>
```
<!-- ここを唯一の真実 (SSOT) にする -->
<Version>1.1.0</Version>
<ApplicationDisplayVersion>1.1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
```
CI/CD の実装例(個別ビルド運用)
GitHub Actions:ターゲット分割
name: build-maui
on:
push:
branches: [ main ]
jobs:
build-ios:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- run: dotnet workload restore
- run: dotnet build YourApp.csproj -c Release -p:TargetFramework=net9.0-ios
build-maccatalyst:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- run: dotnet build YourApp.csproj -c Release -p:TargetFramework=net9.0-maccatalyst
build-windows:
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- run: dotnet build YourApp.csproj -c Release -p:TargetFramework=net9.0-windows10.0.19041
ポイントは、各ジョブで単一の TargetFramework を明示していること。NU1105 を恒久的に回避しつつ、成果物もターゲット別に管理しやすくなります。
トラブル時のチェックリスト
| 確認ポイント | 見る場所 / コマンド | OK の目安 |
|---|---|---|
Version が単一か | .csproj / Directory.Build.props | どのターゲットでも同一値 |
PackageVersion が揃っているか | バイナリログの Properties | 単一の値に評価されている |
古い条件付き PropertyGroup の残骸 | dotnet msbuild -preprocess | 不要なブロックが無い |
| 依存パッケージの版ずれ | dotnet list package --include-transitive | 主要パッケージが統一 |
| ワークロードの整合 | dotnet workload list / update / restore | 不足・旧版がない |
よくある落とし穴
VersionとApplicationDisplayVersionを混同:Versionは NuGet/アセンブリの版。ApplicationDisplayVersionはアプリ UI/ストア表示の版。別物です。ApplicationVersionの取り扱い:Android/Windows は整数のビルド番号を要求(iOS は CFBundleVersion と CFBundleShortVersionString の関係)。ここをターゲット別に切り替える場合でも、Version自体は共通に。- ビルド時の暗黙
Pack:GeneratePackageOnBuildがtrueのままだと、PackageVersionの単一性がさらに重要になります。不要ならオフに。 - 条件の書き方:
Condition="'$(TargetFramework)'=='net9.0-ios'"とCondition="'$(TargetFrameworkIdentifier)'=='.NETCoreApp' and '$(TargetPlatformIdentifier)'=='ios'"を混在させると読みづらくなり、メンテ時に分裂を招きます。
実際の修正ステップ(テンプレに沿って安全に)
- ブランチを切る(fix/nu1105 など)。
.csprojを開き、Versionを 1 箇所に定義。他の場所のVersionは削除。ApplicationDisplayVersionとApplicationVersionも 1 箇所に寄せる。ターゲット別にしたい場合はマニフェストへ移行。- 旧 TFM の条件付き
PropertyGroupを削除。 - 依存パッケージの版を統一(必要なら
Directory.Packages.propsを導入)。 - キャッシュとワークロードをクリーンアップ(前述のコマンド)。
- まず 単一ターゲットでビルドが通るか確認 → その後 マルチターゲットを戻しても通るか検証。
パフォーマンス・メンテ観点のベストプラクティス
- SSOT(Single Source of Truth):
Version、ApplicationDisplayVersion、ApplicationVersionは 1 ファイルに集約。推奨はDirectory.Build.props。 - ターゲット分割の CI:同時ビルド時間を短縮でき、障害点の切り分けも容易。
- テンプレート再生成の活用:新規 MAUI テンプレートを生成して差分比較すると、古い記述の発見が速い。
トラブルの裏側(MSBuild の挙動メモ)
MSBuild はマルチターゲット時、各 TFM ごとにプロジェクトを評価し、同名プロパティに別の値が割り当てられると「単一性が必要なプロパティ」ではエラーを投げます。PackageVersion は Version を継承するため、Version を TFM 条件で変えることは実質的に禁物です。UI 表示やストア要件はマニフェストへ逃がすのが設計として自然です。
Q&A
Q. ApplicationDisplayVersion を iOS と Android で変えたい。どうすれば? A. Info.plist / AndroidManifest.xml に直接記載します。.csproj で条件分岐すると Version/PackageVersion に波及しがちです。 Q. PackageVersion だけ明示すれば安全? A. 原則 はい。Version を TFM 非依存に固定した上で、PackageVersion を単一値で明示すれば NU1105 は回避できます。ただし運用で二重管理にならないように。 Q. それでも直らないときは? A. バイナリログで「どのファイルが上書きしたか」を確認し、Directory.*.props 系や外部 .targets の影響を切り分けます。サブプロジェクトに別の Directory.Build.props がいることもあります。
最終結果
今回は TargetFramework を 1 つに絞る構成へ変更したことで、NU1105 は即時解消。以降の開発・リリースも問題なく継続できました。恒久対策として、バージョン情報の SSOT 化と、ターゲット分割 CI を併用することで、再発可能性を大幅に低減できます。
付録:コマンド早見表
| 目的 | コマンド |
|---|---|
| 依存関係の可視化 | dotnet list package --include-transitive |
| NuGet キャッシュ削除 | dotnet nuget locals all --clear |
| ワークロードの更新 | dotnet workload update |
| ワークロードの復元 | dotnet workload restore |
| ビルドの詳細ログ | dotnet build -v:diag |
| バイナリログ出力 | dotnet build /bl |
| 前処理出力(統合後のプロジェクト) | dotnet msbuild -preprocess:preprocessed.xml |
まとめ:NU1105 は「プロジェクト内の真の版数が 1 つに定まっていない」ことの警告です。Version と PackageVersion を共通化し、OS 固有はマニフェストへ。直近は単一ターゲット ビルドで逃げ、恒久的には SSOT と CI のターゲット分割で堅牢化する。これが .NET MAUI 9.0 世代の実務解です。

コメント