NuGet同梱DLLがPublishにコピーされない原因と解決策:.targetsでCopyToOutputDirectory/CopyToPublishDirectoryを自動化

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/Releasepublishフォルダ(VSの発行先 / dotnet publish の出力先)
主に影響する設定CopyToOutputDirectoryCopyToPublishDirectory(+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) を起点にしているため、参照側の環境差に強い
  • CopyToOutputDirectoryCopyToPublishDirectory を両方セット(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 DLLRID/TFM設計が必要
確実に出力・Publishへ入れたい.targets(build/buildTransitive)反射ロード、プラグイン、外部ツールなどコピー対象や条件の設計は丁寧に

今回の要件は「毎回手動で常にコピーに直すのをやめたい」「Publish漏れをゼロにしたい」なので、パッケージ側でMSBuildを注入する .targets 方式が、再現性・運用性ともに最適解になりやすいです。

まとめ:手動変更を根絶するなら“パッケージ側でコピーを宣言”する

NuGet同梱DLLがPublishに入らない問題は、利用側でプロパティをいじって対処すると、更新・復元・参照関係の変化で簡単に再発します。パッケージ側に .targets(または .props) を同梱し、CopyToOutputDirectory / CopyToPublishDirectory をビルド時に注入する形へ寄せると、次の状態を作れます。

  • 利用側はNuGetを入れるだけで、必要DLLが常に成果物に入る
  • Publish漏れによる本番障害を未然に防げる
  • 依存DLLの増減にもパッケージ更新だけで追従できる

「確実にコピーしたい」を最優先するなら、まずは CopyToPublishDirectory まで含めた .targets を実装し、CLIの dotnet publish で機械的に検証できる状態を作るのが、最短で堅牢です。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次