NuGetパッケージに同梱した依存DLLが、参照先プロジェクトへ追加した途端に「出力ディレクトリにコピー」が“コピーしない”相当になり、Publish(発行)成果物から必要ファイルが抜け落ちる――。この現象の理由と、パッケージ側で確実に自動コピーさせる実装(.targets/.props)を、構成例と現場での落とし穴込みでまとめます。
現象:NuGet同梱DLLがPublish成果物に入らない
カスタムNuGetパッケージに依存DLL(例:サードパーティのDLL、ネイティブDLL、リフレクションで読み込むプラグインDLLなど)を同梱して配布したところ、利用側プロジェクトで次のような問題が起きるケースがあります。
- Visual Studio上では参照できているように見えるのに、実行時に DLLが見つからない エラーになる
- ビルド(bin/Debug や bin/Release)では入ったり入らなかったりして再現が不安定
- 特に Publish(発行) をすると、成果物フォルダに同梱DLLが入らず、配置漏れが起きる
- 毎回、利用側プロジェクトのファイルプロパティで「出力ディレクトリにコピー(Copy to Output Directory)」を 常にコピー に手動変更して回避している
結論から言うと、NuGetで配布したファイルの“コピーする/しない”を、利用側のVisual Studioプロパティに頼って「引き継がせる」設計にすると破綻しやすいです。パッケージ側でMSBuildの定義を同梱し、参照側ビルド時にコピー指示を注入するのが、最も確実で運用負荷も下がります。
まず押さえる:ビルド出力とPublish出力は別物
「ビルドしたら動いたのに、Publishしたら壊れた」というとき、だいたい “ビルド出力(Output)” と “Publish出力(Publish)” の扱いの違いが原因です。
| 観点 | ビルド出力(Output) | Publish出力(Publish) |
|---|---|---|
| 代表的な出力先 | bin/Debug や bin/Release | publishフォルダ(VSの発行先 / dotnet publish の出力先) |
| 主に影響する設定 | CopyToOutputDirectory | CopyToPublishDirectory(+Publishパイプライン) |
| 起きがちな現象 | ローカル実行は動くが、環境差で欠落が露見しにくい | 成果物として必要なファイルが欠落して一発で障害化 |
重要なのは、CopyToOutputDirectory だけだと Publish に入らないケースがあり得ることです。特に「Publishで確実に必要」なDLLなら、最初から CopyToPublishDirectory もセットで面倒を見るのが安全策です。
なぜ「Copy to Output Directory」が維持されないのか
Visual Studioの「出力ディレクトリにコピー(Copy to Output Directory)」は、基本的にプロジェクト内のアイテム(Content/None など)に付くメタデータです。つまり、利用側プロジェクトの .csproj 内に次のような形で“保存される”ことで効力を持ちます。
<ItemGroup>
<None Include="SomeFile.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
しかし、NuGetパッケージ内のファイルは、参照側プロジェクトの .csproj に「物理的に追加されるファイル」とは限りません。PackageReferenceの復元により、ファイルは グローバルパッケージフォルダ に展開され、ビルド時に「参照資産」として扱われます。ここで問題になるのが次の点です。
- 利用側プロジェクトに、“CopyToOutputDirectory=Always” の情報を保存する場所がない(=UIプロパティを変えても、パッケージ更新や復元で揺れやすい)
- 「参照(Reference)」として扱われるのか、「コンテンツ(Content/None)」として扱われるのかで、コピー挙動が変わる
- Publishはビルドと別の収集ロジックでファイルを集めるため、Outputだけに出ていてもPublishに入らないことがある
つまり、“Visual Studioのプロパティを引き継ぐ” という発想がそもそも不安定になりやすいわけです。安定解は、パッケージ側でMSBuildを使って、参照側のビルドプロセスに「このファイルは常にコピーせよ」と宣言することです。
解決の基本方針:NuGetパッケージに .targets / .props を同梱する
ここが本題です。やることはシンプルで、NuGetパッケージ内にMSBuildの .targets(または .props)を入れ、参照側でパッケージが読み込まれたタイミングで、コピー対象のアイテムを追加します。
この方式が強い理由は次の通りです。
- 利用側が手作業でプロパティをいじる必要がなくなる(運用コストゼロ化)
- ビルド・Publishの両方をパッケージ側で一括制御できる
- 複数プロジェクト・複数ソリューションへの展開で差が出にくい
- 「DLLが増えた」「フォルダを変えた」などの変更がパッケージ更新だけで反映できる
.props と .targets の使い分け
どちらでも実現できますが、一般的には「アイテム追加・コピー指定」は .targets が扱いやすいです(読み込みタイミングが後で、他の設定と衝突しにくい)。
| ファイル | 読み込まれるタイミング | 向いている用途 | 注意点 |
|---|---|---|---|
| .props | 比較的早い(プロジェクト評価の早期) | プロパティの既定値、スイッチ類の定義 | 早すぎて上書きされることがある |
| .targets | 比較的遅い(ビルドターゲット評価の後半) | アイテム追加、タスク実行、コピー指示の注入 | ターゲット順序に依存する処理は設計が必要 |
build と buildTransitive の違い
NuGetパッケージの build / buildTransitive フォルダに置くと、自動でインポートされます。違いは「効く範囲」です。
| 配置先 | 効く範囲 | おすすめケース |
|---|---|---|
| build | そのパッケージを直接参照しているプロジェクト | 参照が必ず直参照になる設計、影響範囲を最小化したい |
| buildTransitive | 参照が連鎖(推移)しても効く | 共通基盤パッケージ、SDK-styleで「入れたら全部効く」にしたい |
「別パッケージ経由で参照されることがある」「依存が深くなることがある」なら、buildTransitive に入れておくと事故が減ります。
実装例:.targets でDLLを常にコピー(Output / Publish)
最小構成の例です。パッケージ内のDLLを、ビルド出力とPublish出力の両方に確実に含めます。
シンプルな .targets 例
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<!-- パッケージ内のDLLを常に出力(必要ならPublish)へコピー -->
<Content Include="$(MSBuildThisFileDirectory)..\runtimes\win\lib\netstandard2.0\YourDependency.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
</Content>
</ItemGroup>
</Project>
ポイントは以下です。
$(MSBuildThisFileDirectory)を起点にしているため、参照側の環境差に強いCopyToOutputDirectoryとCopyToPublishDirectoryを両方セット(Publish漏れ対策)- DLLの置き場所は例なので、実際のパッケージ構造に合わせてパスを調整する
実務向け:複数DLLをまとめて対象にする(ワイルドカード)
依存DLLが増減する場合、1ファイルずつ列挙するとメンテが苦しくなります。フォルダごとコピー対象にしてしまうのが運用上ラクです。
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<!-- “非公開”アイテム名として _ で始めると意図が伝わりやすい -->
<_MyPackageDeps Include="$(MSBuildThisFileDirectory)..\runtimes\win\lib\netstandard2.0\*.dll" />
<None Include="@(_MyPackageDeps)">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<!-- Solution Explorer に見せたくない場合 -->
<Visible>false</Visible>
</None>
</ItemGroup>
</Project>
「常にコピー(Always)」は確実性が高い一方で、ビルドのたびにコピーが走りやすくなります。更新頻度が低いDLLなら PreserveNewest にして、差分コピーに寄せるのが現場ではバランス良いことが多いです。
| 設定値 | 挙動 | 向いているケース |
|---|---|---|
| Always | 毎回コピー | 絶対に漏らしたくない、環境差で壊れやすい、初期は原因切り分け中 |
| PreserveNewest | 更新があるときだけコピー | 安定運用、ビルド時間を抑えたい、依存DLLが多い |
パッケージ構成:どこに何を置くべきか
.targets方式を成立させるには、NuGetパッケージ内の配置が重要です。代表的な構成例を示します。
構成イメージ
/build/または/buildTransitive/にMyPackage.targets- DLL本体は用途に応じて配置(例:
/runtimes/...や/contentFiles/...、あるいは/lib/...)
よく使う配置パターン
| 同梱したいファイルの性質 | 推奨配置 | 理由 |
|---|---|---|
| コンパイル参照される managed DLL(C#の参照として自然) | lib/<TFM>/ | NuGetが参照として解決しやすく、通常は自動で出力・Publish対象になりやすい |
| RID依存の managed DLL(Windows専用など) | runtimes/<RID>/lib/<TFM>/ | 実行環境に応じて正しい資産が選択されやすい |
| ネイティブDLL(native) | runtimes/<RID>/native/ | ネイティブ資産として扱うのが自然。とはいえコピー制御が必要なら.targetsで補強 |
| 参照としては不要だが“同梱必須”(反射ロード、外部ツール、テンプレ) | contentFiles/ または任意フォルダ+.targets | 「配布はしたいが参照にはしたくない」場合に.targetsが強い |
今回のように「とにかくPublish成果物へ確実に入れたい」なら、配置をどうするにせよ、.targetsで CopyToPublishDirectory を握るのが一番トラブルを減らします。
SDK-styleでの“パック(Pack)”例:.csprojに同梱する場合
NuGetパッケージをSDK-styleの dotnet pack / Visual StudioのPackで作る場合、.csproj 側で「どのファイルをどこへ入れるか」を指定できます。以下は一例です。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<!-- .targets を buildTransitive に入れる -->
<None Include="buildTransitive\MyPackage.targets"
Pack="true"
PackagePath="buildTransitive\" />
<!-- 同梱DLLを runtimes 配下に入れる(例) -->
<None Include="runtimes\win\lib\netstandard2.0\YourDependency.dll"
Pack="true"
PackagePath="runtimes/win/lib/netstandard2.0/" />
</ItemGroup>
</Project>
この形にしておくと、パッケージ更新だけで参照側の挙動も統一できます。利用側は「インストールしたら勝手に直る」状態にできるのが最大のメリットです。
Publishで確実に入れるためのチェックポイント
「ビルド出力には出たがPublishに入らない」を潰すための実務チェック項目です。
| チェック | 見る場所 | よくある原因 | 対策 |
|---|---|---|---|
| Publish先にDLLが出ているか | 発行先フォルダ | CopyToPublishDirectory未設定 | CopyToPublishDirectory を付与 |
| buildTransitiveが必要か | 参照関係 | 直参照ではなく、別パッケージ経由で参照されている | buildTransitive へ移動 |
| .targetsのパスが正しいか | .targets内のInclude | 相対パスの起点がズレている | $(MSBuildThisFileDirectory) 起点に統一 |
| TFM/RIDが合っているか | runtimes/libの階層 | 参照側のTargetFramework/RuntimeIdentifierと不一致 | 配置を見直す、条件分岐を入れる |
| 同名ファイル衝突がないか | 出力フォルダ | 別パッケージも同じDLL名をコピーして上書き | ファイル名を分ける、条件を絞る |
動作確認の手順:手元で再現と検証を固定化する
「VSの発行だけで見ている」と原因が見えにくいことがあります。CLIで検証を固定化しておくと、CIでも再現できます。
推奨コマンド例
dotnet restore
dotnet build -c Release
# Publish(例:フォルダ出力)
dotnet publish -c Release -o ._publish
Publishの出力先(.\_publish)に、同梱DLLが入っているかを必ず確認してください。ここで入っていない場合、ほぼ間違いなく CopyToPublishDirectory 側の指定か、.targetsの取り込み(build/buildTransitive)が原因です。
トラブルシューティング:ハマりやすい落とし穴
「.targetsを入れたのに効いていない」
- パッケージ内の配置が間違っている(
build/またはbuildTransitive/直下に置けていない) - パッケージを更新したつもりで、参照側が古いキャッシュを掴んでいる
- 参照が推移しているのに
buildに置いてしまっている
対策としては「nupkgを一度zipとして開いて、意図した場所にファイルが入っているか」を最初に確認するのが近道です。
「ビルド出力にはあるがPublishだけ無い」
この症状はかなり多いです。CopyToOutputDirectory しか付けていない、またはアイテム種別がPublishの収集対象に乗っていない可能性があります。まずは次を徹底します。
CopyToPublishDirectoryを必ず付けるNoneまたはContentとしてアイテムを追加しているか確認する
「Single-file Publishで挙動が違う」
単一ファイル発行(single-file)では、成果物が1つのexeに束ねられる構成になり得ます。同梱DLLを「外部ファイルとして必ず置きたい」場合は、プロジェクトの発行設定次第で調整が必要です。パッケージ側で一律に決め打ちしにくい領域なので、次の考え方が安全です。
- まずは
CopyToPublishDirectoryで「Publishフォルダに出す」を保証する - single-fileの方針(同梱ファイルを束ねる/束ねない)は、利用側アプリの要件で最終決定する
「ネイティブDLLが見つからない(x86/x64/ARM64など)」
ネイティブDLLはCPUアーキテクチャやRIDの違いで配置が変わります。安定運用のコツは次の2つです。
runtimes/<RID>/native/など、RIDごとに分けて置く- .targets側で RuntimeIdentifierに応じてコピー対象を切り替える(必要なら条件分岐)
例として、win-x64 のときだけコピーする条件分岐は次のように書けます。
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'win-x64'">
<None Include="$(MSBuildThisFileDirectory)..\runtimes\win-x64\native\*.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<Visible>false</Visible>
</None>
</ItemGroup>
</Project>
別解も知っておく:本当に“同梱”が最適か?
ここまで .targets 方式を主軸に解説しましたが、実務では「そもそも同梱より自然なやり方がある」ケースもあります。判断のヒントをまとめます。
| やりたいこと | 候補 | 向いている状況 | 注意点 |
|---|---|---|---|
| 依存DLLを参照として扱いたい | lib配下に入れる / 依存パッケージとして分離 | 通常の参照・コンパイルで使う | 依存関係管理は明確だが、同梱より管理対象が増える |
| OSやアーキ依存の資産を切り替えたい | runtimes配下でRID別に管理 | ネイティブDLLや環境依存のmanaged DLL | RID/TFM設計が必要 |
| 確実に出力・Publishへ入れたい | .targets(build/buildTransitive) | 反射ロード、プラグイン、外部ツールなど | コピー対象や条件の設計は丁寧に |
今回の要件は「毎回手動で常にコピーに直すのをやめたい」「Publish漏れをゼロにしたい」なので、パッケージ側でMSBuildを注入する .targets 方式が、再現性・運用性ともに最適解になりやすいです。
まとめ:手動変更を根絶するなら“パッケージ側でコピーを宣言”する
NuGet同梱DLLがPublishに入らない問題は、利用側でプロパティをいじって対処すると、更新・復元・参照関係の変化で簡単に再発します。パッケージ側に .targets(または .props) を同梱し、CopyToOutputDirectory / CopyToPublishDirectory をビルド時に注入する形へ寄せると、次の状態を作れます。
- 利用側はNuGetを入れるだけで、必要DLLが常に成果物に入る
- Publish漏れによる本番障害を未然に防げる
- 依存DLLの増減にもパッケージ更新だけで追従できる
「確実にコピーしたい」を最優先するなら、まずは CopyToPublishDirectory まで含めた .targets を実装し、CLIの dotnet publish で機械的に検証できる状態を作るのが、最短で堅牢です。

コメント