連載Index(読む順・公開済みリンクが最新)
C#達人への道 連載Index|読む順・公開記事一覧【S00】
Visual StudioでF5キーを押すと、作ったアプリケーションが起動します。
普段はそれだけで困りません。
ただ、ソリューション内にプロジェクトが増えてくると、急に分かりにくくなります。
- 今起動しているEXEは、どこにあるのか
- EXEの横にあるDLLは、どこから来たのか
-
binとobjは何が違うのか - 別プロジェクトで作ったDLLを、アプリケーションはどうやって見つけるのか
- 開発中は読めた設定ファイルが、インストール後に読めなくなるのはなぜか
このあたりは別々の話に見えますが、実際にはすべてつながっています。
この記事では、F5で起動しているEXEの場所から、複数プロジェクトのDLLが集まる仕組みまでを順番に見ていきます。
先に結論
最初に全体像をまとめます。
| 知りたいこと | 結論 |
|---|---|
| EXEやDLLはどこに作られるか | 基本的に各プロジェクトのbin以下 |
objは何か |
ビルド途中で使う中間ファイルの置き場 |
| DLLプロジェクトで作ったDLLはどこに出るか | そのDLLプロジェクト自身のbin以下 |
| アプリ側へDLLが来る理由 |
ProjectReferenceによって参照関係が設定されているため |
| ファイル名だけで読める基準 | EXEの場所ではなく、カレントディレクトリ |
| EXEと同じ場所を基準にしたい |
AppContext.BaseDirectoryを使う |
| 書き換える設定やログの保存先 | AppDataなど、書き込み可能な場所 |
ここから、順番に見ていきます。
F5で起動しているEXEはどこにあるのか
.NET FrameworkのデスクトップアプリケーションをDebugビルドした場合、通常は次の場所にEXEが作られます。
MyApp
├─ bin
│ └─ Debug
│ ├─ MyApp.exe
│ ├─ MyApp.exe.config
│ ├─ MyLibrary.dll
│ └─ OtherLibrary.dll
├─ obj
└─ MyApp.csproj
Visual StudioからF5で起動した場合、基本的にはbin\DebugにあるMyApp.exeが実行されます。
Releaseビルドなら、出力先はbin\Releaseです。
bin
├─ Debug
└─ Release
.NET 8など、現在の.NETではターゲットフレームワーク名のフォルダーが追加されます。
MyApp
└─ bin
└─ Debug
└─ net8.0-windows
├─ MyApp.exe
├─ MyApp.dll
├─ MyApp.deps.json
├─ MyApp.runtimeconfig.json
└─ MyLibrary.dll
.NET Frameworkと現在の.NETでは出力されるファイルが少し違います。
ただし、最初は次の理解で十分です。
実行に必要なファイルは、基本的にアプリケーションプロジェクトの
bin以下へ集まる。
binとobjは何が違うのか
ビルドすると、プロジェクト内にbinとobjが作られます。
| フォルダー | 役割 |
|---|---|
bin |
EXEやDLLなど、ビルドして作られたファイル |
obj |
コンパイル途中で使う中間ファイル |
実行ファイルやDLLを確認したいときは、まずbinを見ます。
objは、コンパイラーやMSBuildがビルド途中で使う場所です。通常、obj内のファイルを直接実行したり、配布したりすることはありません。
ビルド結果がおかしいときに、binとobjを削除して再ビルドすることがあります。
これは、以前のビルドで残ったファイルをいったん消し、最初から作り直すためです。
binとobjに入っているのは、ビルド時に作られたファイルです。
通常は削除しても、次回のビルドで作り直されます。ただし、手作業で置いたファイルがある場合は一緒に消えるため、その点だけ確認しておくと安心です。
ソリューションとプロジェクトの違い
プロジェクトが1つだけの間は、ソリューションとプロジェクトの違いを意識しなくても困りません。
複数プロジェクトになると、この違いが大切になります。
MySolution.sln
├─ MyApp
│ └─ MyApp.csproj
├─ MyLibrary
│ └─ MyLibrary.csproj
└─ MyDataAccess
└─ MyDataAccess.csproj
この例では、1つのソリューションに3つのプロジェクトがあります。
| プロジェクト | 役割 |
|---|---|
MyApp |
画面や起動処理を持つアプリケーション |
MyLibrary |
共通処理を持つクラスライブラリ |
MyDataAccess |
データアクセスを担当するクラスライブラリ |
.slnは、複数のプロジェクトをまとめて管理するためのファイルです。
一方、.csprojには、そのプロジェクトの次のような情報が書かれています。
- どの.NETを使うか
- どのファイルをビルドするか
- どのプロジェクトやパッケージを参照するか
- どこへ出力するか
- ビルド時に追加で何をするか
つまり、ビルドの単位は基本的にプロジェクトです。
binとobjはプロジェクトごとに作られる
複数プロジェクトのソリューションでも、各プロジェクトの仕組みは同じです。
MySolution
├─ MyApp
│ ├─ bin
│ ├─ obj
│ └─ MyApp.csproj
│
├─ MyLibrary
│ ├─ bin
│ ├─ obj
│ └─ MyLibrary.csproj
│
└─ MyDataAccess
├─ bin
├─ obj
└─ MyDataAccess.csproj
MyLibraryをビルドすると、まずMyLibrary自身の出力先へDLLが作られます。
MyLibrary
└─ bin
└─ Debug
└─ MyLibrary.dll
ここで、最初に引っかかりやすい点があります。
MyAppからMyLibraryを使うなら、実行時にはMyApp側にもMyLibrary.dllが必要です。
MyApp
└─ bin
└─ Debug
├─ MyApp.exe
└─ MyLibrary.dll
では、別のプロジェクトで作られたDLLは、どうやってMyApp側へ来るのでしょうか。
そこで使うのがProjectReferenceです。
ProjectReferenceでプロジェクト同士をつなぐ
アプリケーションから別プロジェクトを使う場合、通常はプロジェクト参照を追加します。
.csprojには、次のように記録されます。
<ItemGroup>
<ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
<ProjectReference Include="..\MyDataAccess\MyDataAccess.csproj" />
</ItemGroup>
ProjectReferenceが設定されていると、MSBuildに次の関係が伝わります。
MyApp
├─ MyLibraryが必要
└─ MyDataAccessが必要
ビルドすると、次の順番で処理が進みます。
- 参照先のプロジェクトを先にビルドする
-
MyLibrary.dllなどを作成する - アプリケーションのビルドに必要なDLLとして参照する
- 実行に必要なDLLを
MyApp側の出力先へ配置する -
MyAppをビルドする
最終的には、アプリケーション側のbinへ必要なDLLが集まります。
MyApp
└─ bin
└─ Debug
├─ MyApp.exe
├─ MyApp.dll
├─ MyLibrary.dll
└─ MyDataAccess.dll
ここで大切なのは、ProjectReferenceが単なるコピー設定ではないことです。
ProjectReferenceには、次の役割があります。
- プロジェクト同士の依存関係を伝える
- ビルド順序を決める
- 参照先をビルドして作られた最新のDLLを使う
- 実行に必要なDLLをアプリ側へ配置する
DLLを手作業でコピーする必要がなくなり、古いDLLを使ってしまうミスも減らせます。
DLLを直接参照する場合との違い
すでに作られているDLLを、ファイルとして直接参照することもできます。
<ItemGroup>
<Reference Include="MyLibrary">
<HintPath>..\Libraries\MyLibrary.dll</HintPath>
</Reference>
</ItemGroup>
使い分けは次のように考えると分かりやすくなります。
| 参照方法 | 主な用途 |
|---|---|
ProjectReference |
同じソリューション内にソースコードがある |
| DLLの直接参照 | 外部からDLLだけ提供されている |
PackageReference |
NuGetパッケージを使う |
同じソリューション内にプロジェクトがあるのにDLLを直接参照すると、次の問題が起きやすくなります。
- 参照先を修正してもDLLが更新されない
- 古いDLLを使い続ける
- Debug版とRelease版を取り違える
- DLLのコピー作業が増える
- どちらを先にビルドすべきか分かりにくくなる
同じソリューション内で管理しているプロジェクトなら、基本はProjectReferenceを使います。
EXEと同じ場所にあるファイルは、名前だけで読めるのか
次のように、EXEと設定ファイルが同じフォルダーにあるとします。
MyApp.exe
settings.json
次のコードで読み込めることがあります。
string json = File.ReadAllText("settings.json");
ただし、このコードは「EXEと同じフォルダーのsettings.jsonを読む」とは限りません。
settings.jsonのような相対パスは、カレントディレクトリを基準に探されます。
現在のカレントディレクトリは、次のコードで確認できます。
string currentDirectory = Directory.GetCurrentDirectory();
Console.WriteLine(currentDirectory);
Visual StudioからF5で実行している間は、カレントディレクトリとビルド出力先が同じになることが多いため、問題に気付きにくいところです。
しかし、次のような起動方法では、カレントディレクトリが変わることがあります。
- ショートカットから起動する
- PowerShellやコマンドプロンプトから起動する
- 別アプリケーションから起動する
- タスクスケジューラから起動する
- Windowsサービスとして実行する
そのため、開発中は読み込めたのに、配布後はファイルが見つからないということがあります。
アプリケーションのフォルダーを基準にする
アプリケーションと一緒に置いたファイルを確実に読み込みたい場合は、AppContext.BaseDirectoryを使います。
string settingsPath = Path.Combine(
AppContext.BaseDirectory,
"settings.json");
string json = File.ReadAllText(settingsPath);
サブフォルダーに置く場合も、Path.Combineで組み立てます。
MyApp.exe
Settings
└─ settings.json
string settingsPath = Path.Combine(
AppContext.BaseDirectory,
"Settings",
"settings.json");
string json = File.ReadAllText(settingsPath);
文字列を直接連結するより、Path.Combineを使った方が、パスの区切りを安全に扱えます。
// 文字列を直接連結する例
string settingsPath =
AppContext.BaseDirectory + "\\Settings\\settings.json";
// Path.Combineを使う例
string settingsPath = Path.Combine(
AppContext.BaseDirectory,
"Settings",
"settings.json");
設定ファイルは自動でbinへ入るとは限らない
参照しているDLLは、通常、ビルド時にアプリケーションの出力先へ配置されます。
一方で、JSONやCSV、画像などは、プロジェクトへ追加しただけではbinへコピーされないことがあります。
例えば、settings.jsonを出力先へコピーしたい場合は、.csprojへ次のように設定します。
<ItemGroup>
<None Update="settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
主な設定値は次のとおりです。
| 設定値 | 動作 |
|---|---|
Always |
ビルドするたびにコピーする |
PreserveNewest |
コピー元の方が新しい場合だけコピーする |
Never |
コピーしない |
通常は、必要なときだけコピーされるPreserveNewestが使いやすい設定です。
Visual Studioのプロパティ画面では、「出力ディレクトリにコピー」を「新しい場合はコピーする」に設定すると、同じ内容が.csprojへ反映されます。
Program Filesにインストールした後は保存先を分ける
開発中は、EXEと同じ場所へ設定ファイルやログを書き込んでも動くことがあります。
しかし、アプリケーションをProgram Filesへインストールすると、同じコードが失敗する場合があります。
C:\Program Files\MyCompany\MyApp
Program Filesは、通常のユーザー権限で自由に書き換える場所ではないからです。
保存先は、ファイルの用途で分けます。
| ファイル | 保存先の例 |
|---|---|
| EXE、DLL | Program Filesなどのインストール先 |
| 書き換えない初期設定 | インストール先 |
| ユーザーごとの設定 | AppData |
| ログ、キャッシュ | LocalAppData |
| 全ユーザー共通データ | ProgramDataなど |
ユーザーごとの設定保存先は、次のように取得できます。
string appDataDirectory = Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData);
string settingsDirectory = Path.Combine(
appDataDirectory,
"MyCompany",
"MyApp");
Directory.CreateDirectory(settingsDirectory);
string settingsPath = Path.Combine(
settingsDirectory,
"settings.json");
ログやキャッシュなど、端末内だけで使うデータにはLocalApplicationDataが向いています。
string localAppDataDirectory = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData);
string logDirectory = Path.Combine(
localAppDataDirectory,
"MyCompany",
"MyApp",
"Logs");
Directory.CreateDirectory(logDirectory);
保存先は「EXEと同じ場所に置けるか」ではなく、実行中に書き換えるかどうかで考えると迷いにくくなります。
出力先が思っていた場所と違うときに確認する順番
ビルド結果が思っていた場所に出ていない場合、最初からビルド後イベントだけを探す必要はありません。
次の順番で確認すると、原因を見つけやすくなります。
1. プロジェクトのプロパティ
まず、DebugとReleaseのどちらでビルドしているかを確認します。
同じプロジェクトでも、構成によって出力先が変わります。
2. csprojの出力設定
.csprojにOutputPathなどが設定されていないか確認します。
<PropertyGroup>
<OutputPath>..\Output\</OutputPath>
</PropertyGroup>
3. Directory.Build.propsとDirectory.Build.targets
上位フォルダーに次のファイルがあると、複数プロジェクトへ共通設定が適用されます。
Directory.Build.props
Directory.Build.targets
.csprojだけ見ても設定が見つからない場合は、上位フォルダーも確認します。
4. ビルド前後の処理
ビルド後にファイルをコピーしている場合があります。
<Target Name="CopySettings" AfterTargets="Build">
<Copy
SourceFiles="settings.json"
DestinationFolder="$(OutputPath)" />
</Target>
Visual Studioのビルドイベントだけでなく、MSBuildのTargetとして書かれている場合もあります。
5. ビルドを実行しているスクリプト
PowerShell、バッチファイル、GitHub Actionsなどから、出力先を上書きしている場合があります。
.csprojに原因がなければ、実際にビルドを開始しているコマンドまで確認します。
症状から確認場所を絞る
最後に、よくある症状と確認場所をまとめます。
| 症状 | 最初に確認する場所 |
|---|---|
| 修正したはずなのに動きが古い | 起動しているEXEの場所、Debug/Release、binの更新日時 |
| DLLが見つからない |
ProjectReference、参照先のビルド結果、アプリ側のbin
|
| JSONや画像が見つからない |
CopyToOutputDirectory、AppContext.BaseDirectory
|
| 開発中は保存できたが、インストール後は失敗する | Program Filesへ書き込んでいないか |
| 出力先がプロジェクト設定と違う |
Directory.Build.props、Directory.Build.targets、ビルドスクリプト |
| DLLを直してもアプリへ反映されない | DLL直接参照ではなくProjectReferenceになっているか |
症状に合わせて見る場所を絞れば、「何となくbinを消して試す」だけでなく、原因を考えながら確認できます。
まとめ
C#/.NETのビルドでは、プロジェクトごとにbinとobjが作られます。
プロジェクト
├─ bin
│ └─ 実行や配布に使うビルド結果
└─ obj
└─ ビルド途中で使う中間ファイル
複数プロジェクトの場合も、仕組みは同じです。
ソリューション
├─ アプリケーションプロジェクト
│ ├─ bin
│ └─ obj
└─ クラスライブラリプロジェクト
├─ bin
└─ obj
アプリケーションから別プロジェクトを使う場合は、ProjectReferenceを設定します。
<ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
これにより、MSBuildがビルド順序と参照関係を管理し、必要なDLLをアプリケーション側の出力先へ配置します。
ファイルを読むときは、次の違いも重要です。
// カレントディレクトリを基準にする
string relativePath = "settings.json";
// アプリケーション側のフォルダーを基準にする
string applicationPath = Path.Combine(
AppContext.BaseDirectory,
"settings.json");
最後に、今回の内容を一言でまとめると次のようになります。
各プロジェクトは自分の
binにEXEやDLLを出力し、アプリケーションに必要なDLLはProjectReferenceによってアプリ側のbinへ集められる。
この流れが分かると、DLLが見つからない、設定ファイルを読めない、インストール後に書き込めないといった問題でも、どこを確認すればよいか判断しやすくなります。
連載Index(読む順・公開済みリンクが最新)
C#達人への道 連載Index|読む順・公開記事一覧【S00】