wifrstfasnriov
@wifrstfasnriov (KA TO)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

exeと同階層にあるファイルをサブフォルダにまとめたい

解決したいこと

Visual Studio 2022でWindowsフォームアプリを作っています。
プロジェクトの発行をすると、出力フォルダ(今回はpublish_folderとします)にexeやdllなどが出力されます。

publish_folder
 └appX.exe
 └appX.dll 
 └aaa.dll
 └bbb.dll
 └appX.dll.config

実行時、ユーザーはexeをダブルクリックして起動しますが、このままだとexeがほかのファイルに埋もれてしまって見つけにくいです。(上のイメージはかなり簡略化しており。実際のファイルの数は非常に多いです。)
そこで、フォルダを開けばexeがすぐに見つかるよう、exe以外のファイルをサブフォルダにまとめてしまいたいと考えています。

publish_folder
└appX.exe
└bin
  └appX.dll
  └aaa.dll
  └bbb.dll
  └appX.dll.config

前提・環境

  • Visual Studio 2022
  • Windows Form App
  • .NET6.0
  • exeにdllを全て埋め込んでしまう単一ファイルでの発行も可能ですが、この方法は使わずに単にフォルダ分けで対応したいと考えています。
  • installerを作ればそもそもユーザーにアプリケーションディレクトリを触らせずに済みますが、今回はそのまま使わせるケースを想定しています。

自分で試したこと

しかし単にサブフォルダを作って移動しただけでは当然ですがexeから必要なファイルが参照できず起動できません。
いくつかのサイトを参考に

以下の通り方針を立てました。

  1. appX.exe.configからbinフォルダを参照させるよう<probing privatePath="bin"/> を追記
  2. appX.dll.configからbinフォルダを参照させるよう<probing privatePath="bin"/> を追記
  3. appX.exe.configを新規作成してbinフォルダを参照させるよう<probing privatePath="bin"/> を追記

どれもうまくいきませんでした。

appX.exe.configからbinフォルダを参照させる

そもそもappX.exe.configが存在しません。ソリューションエクスプローラー上でapp.configは作っています。中身もいろいろ書いて使っています。(ユーザー設定もアプリケーション設定も)
参考2によると、このapp.configからappX.exe.configが生成されるそうですが、発行しても生成されません。代わりにappX.dll.configが生成され、こちらにapp.configの中身が反映されています。

appX.dll.configからbinフォルダを参照させる

appX.exe.configがないならappX.dll.configに書くしかないかと考えこちらに<probing privatePath="bin"/> を追記しました。
(ちなみに、<probing privatePath="bin"/>の親要素であるruntimeとassemblyBinding も追記しました。)

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<configSections>
  <sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=aaa" >
   <section name="appX.My.MySettings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=aaa" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
  </sectionGroup>
  <sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=aaa" >
   <section name="appX.My.MySettings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=aaa" requirePermission="false" />
  </sectionGroup>
 </configSections>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <probing privatePath="bin" />
    </assemblyBinding>
  </runtime>
 <connectionStrings />
 <userSettings>
  <appX.My.MySettings>
   <setting name="path_database_folder" serializeAs="String">
    <value />
   </setting>
  </appX.My.MySettings>
 </userSettings>
 <applicationSettings>
  <appX.My.MySettings>
   <setting name="is_development" serializeAs="String">
    <value>False</value>
   </setting>
  </appX.My.MySettings>
 </applicationSettings>
</configuration>

同時に、appX.dllとappX.dll.configをpublish_folder直下に移動しました。

publish_folder
└appX.exe
└appX.dll
└appX.dll.config
└bin
  └aaa.dll
  └bbb.dll

しかしこれでもうまくいかず、.NET Runtimeをダウンロードするように指示されます。
image.png

runtimeはbinフォルダに入れています(配置モードを自己完結モードにしている)。
image.png

appX.exeからappX.dllを参照してからappX.dllからruntimeを探してくれればよいのですが、そうではなくappX.exeから探すので見つからないよと言われるのかなと思いました。

ではruntimeもpublish_folder直下に上げたらよいかと思わなくもないですが、どれを動かせばよいのかわからないのと、動かしたらdllの参照対象から外れてしまったりするんじゃないかとも思われ、やりたくありません。そもそも、もともとのやりたいことがexeをほかのファイルに埋もれさせないことだったので、その目的から遠ざかってしまいます。

appX.exe.configを新規作成してbinフォルダを参照させるよう<probing privatePath="bin"/> を追記

紆余曲折を経ましたが、やはりappX.exe.configからbinフォルダを参照するしかないと思いました。
app.exe.configが自動生成されないなら仕方ないと自分で作り、以下のように記載しました。probing要素を書くために必要な情報だけ書いたものです。

appX.exe.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <probing privatePath="bin" />
    </assemblyBinding>
  </runtime>
</configuration>

これをpublish_folder直下に置きました。現状のフォルダ構造は以下の通りです。

publish_folder
└appX.exe
└appX.exe.config
└bin
  └aaa.dll
  └bbb.dll
  └appX.dll
  └appX.dll.config

この状態でappX.exeをダブルクリックしても何も起きません。
やりたいことはexe以外をサブフォルダにまとめたいというよくありそうかつ簡単そうなことなのですが、万策尽きました。方法をご存じの方、ご教示いただけますと幸いです。

何卒よろしくお願いいたします。

1

4Answer

プログラムを変えてよいのであれば、Assembly Resolver を利用することで任意の参照先を指定することができます。
このイベントはDLLなどを読み込もうとした結果、参照エラーとなる場合に、エラーを発行する前に呼ばれます。ここで参照先を解決することができれば、正常に動作すると思います。

        [STAThread]
        public static void Main()
        {
            AppDomain.CurrentDomain.AssemblyResolve += Resolver;

            App app = new App();
            app.InitializeComponent();
            app.Run();
        }

        private static Assembly Resolver(object sender, ResolveEventArgs args)
        {
            string assemblyName = args.Name.Split(new[] { ',' }, 2)[0] + ".dll";
            string assemblyPath = Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
                                                       "bin",
                                                       assemblyName);

            return File.Exists(assemblyPath)
                       ? Assembly.LoadFile(assemblyPath)
                       : null;
        }
1Like

Comments

  1. @wifrstfasnriov

    Questioner

    こんな方法があるのですね…。
    参照エラー前提のコードに少し抵抗はありますが十分ありだと思いました。
    質問をしておいてなんなんですが、exeが「探しにくい」程度といえば程度の課題なので、exeと別階層にdllを置くことがべき論で否定されるならそれに従った方がよいかなとも考えています。
    別階層におくべきかも併せて検討しようと思いますが、別階層に置くことにした場合はかなり優先度の高い実装方法です。大変参考になりました。ありがとうございます。

実行時、ユーザーはexeをダブルクリックして起動しますが、このままだとexeがほかのファイルに埋もれてしまって見つけにくいです。
(中略)
そこで、フォルダを開けばexeがすぐに見つかるよう、exe以外のファイルをサブフォルダにまとめてしまいたいと考えています。

XY問題になってないですか?
そんなトリッキーな事をするより、デスクトップにショートカットでも作った方が早いと思いますが。

1Like

Comments

  1. @wifrstfasnriov

    Questioner

    ご回答いただきありがとうございます。
    仰る通り、デスクトップにショートカットを作るのが理想だと思います。一方で、今回のアプリは2通りの配布方法を考えています。
    1.set up project(installer)を配布
    installするとデスクトップにショートカットができるので、実行環境としては文句無し。可能な限りこの方法をとってほしいが、installerの実行にはPCの管理者権限が必要。
    2.zipを解凍してその中のexeをダブルクリックで起動
    1の方法が使えない(社用PCなどで自分のPCの管理者権限を持っていない)ユーザーが使用。

    2の配布方法で今回の質問が課題になります。
    前提の項にほんの少しだけ書いていたのですが、言葉足らずでした。

Qiitaにこんな記事があります

C#.NETのexeでdllファイルを別フォルダーにまとめる
https://qiita.com/waokitsune/items/0b64e5d87d4b5f7435ed

具体的にはApp.Config内のprobing privatePath=対象のフォルダ です。
僕的には別にトリッキーでも何でもない普通のニーズだと思う。

検証しました。

僭越ながら僕にとっても重要だったんで。
.net7で使える為、.net6.0以前でも使えるのではないかと思います。
→ChatGPTに聞いたところ、.NET Framework 2.0以降で使えるとの事です。

公式リファレンス
アセンブリの場所の指定

まず、提示した記事内にはないですが
App.configを作成します
image.png

image.png

<probing privatePath="bin" />を追加します。

binフォルダを作成し、dllを全部突っ込みます。
image.png

ビルドすれば完了です。
Dllを使った機能も正常に動作する事を検証済みです。

これで保守管理がしにくい何てことは無いと思います。まあ業務でそういうケースが無いとも言い切れないですが、おそらく個人開発レベルなので。

1Like

Comments

  1. 質問は.NET6なので、その方法は多分使用できません。
    Assembly Resolverを使用する方法でも、エントリポイントDLLはexeと同一フォルダに置く必要があります。また、ビルドイベントの調整等も必要になり、ライブラリパスに起因する問題が発生しないとも限りません。また、exeのあるフォルダをユーザーに直接触らせる事自体もトラブルの元です。(エクスプローラーは割と簡単にファイルを移動出来てしまうので)
    保守・サポート等のコストが増える可能性があります。

    そこまで考慮してDLLを分ける必要があるか?と言われると自分は無いと思うので、今回の問題の解決方法としてはトリッキーと評しました。ショートカットを作成する方がずっとシンプルです。多数に配布する前提なら、インストーラーを作成した方がよいと思います。
  2. @radian-jp 
    .net7.0 で普通に使用可能でした。
    大した案件でもないので、出来るだけちきんと検証すべきかと思いますね。

    それと、dllを直接触らせたりする事自体トラブルになりかねないと僕は思います。exeは仮にバイナリ弄ったとしても「あ、壊れたから差し替え」で済むと思うんで。
    まあフリーソフトレベルの認識ですが。
  3. @wifrstfasnriov

    Questioner

    検証まで、ありがとうございます。
    貼っていただいた記事は私も参考にしました(内容的には参考1と同じです)。
    方針3で同じことをやったつもりだったのですが、起動できませんでした。.NETのバージョンも関係するんでしょうか。
    皆様からいただいたコメントから、サブフォルダはやらない方がよいかと思いました。やらない方がよいという結論が出ただけで満足です。
    ありがとうございました。
  4. 一応こちらでも検証してみましたが、やはり.NET6では起動不可でした。
    .NET Framework4.8では可能です。

    https://stackoverflow.com/questions/56844233/additional-probing-paths-for-net-core-3-migration
    https://stackoverflow.com/questions/70214477/net-6-c-windowsforms-loading-dlls-from-a-sub-directory-using-probing-does

    既存の質問を見ても、.NET Core系(.NET6含む)では無理そうに見えます。
    runtimeconfig.jsonのadditionalProbingPathsを使用する方法もあるようですが、こちらはまったく同じように出来る訳ではなく、手間が掛かりそうです。

exeがdllを探す方法はいろいろと複雑ですが、基本的にはexeと同じフォルダにdllがないとダメだと思った方がいいです。
なので

publish_folder
 └appX.exeのショートカット
 └bin
  └appX.exe
  └appX.dll 
  └aaa.dll
  └bbb.dll
  └appX.dll.config

とするのがお手軽かつ安全だと思います。
(つまり @radian-jp さんとほぼ同意見)

0Like

Comments

  1. @wifrstfasnriov

    Questioner

    ありがとうございます。
    "基本的にはexeと同じフォルダにdllがないとダメだと思った方がいい"
    もしかしたらこの一文が私が一番欲しかった回答かもしれません。サブフォルダ作る程度のことみんなやってるはず(むしろ多数派だろうと思ってました)という発想があり質問しましたが、「やらないのが普通。そういうもの。」と言われればそれだけで受け入れられます。

    またショートカットを置く方法は試したことがありましたが、少し気になるところがありexe階層自体をいじれた方がきれいだと思って質問しました。
    ショートカットは相対パスで設定することができず絶対パスでしか設定できないため、設定するアドレスは%windir%\explorer.exe "\appX.exe"となります。
    気になったのは以下2点です。
    1. explorer.exeが既定の場所にある前提のショートカットなので環境に依存してしまうのでは?という懸念。
    環境に応じて適当なパスになるようにするための%windir%なので、おそらくこの指定方法で起動できない環境などないのだろうとは思うが、どこまで信用してよいかわからない。
    2. 引数でappX.exeのパスを渡しているだけで実行しているのはexplorer.exeなので、アイコンがexplorer.exeのものになる。
    せっかくアイコン用意してappX.exeに設定したのに残念。

    2つとも大した懸念点ではないので、サブフォルダを作ることが通常ないのであればショートカットでよいかなと思いました。
  2. (そこまでする価値があるかは置いといて)
    「appX.exeを起動するだけのランチャーを作る」というのがわりとお手軽な方法かもしれません。

    1. フォームアプリの新規プロジェクト「appX_launcher」を作成
    2. Form1.csやForm1.designer.csを削除してソースファイルはProgram.csだけにする。
    3. Programe.csのMain()に、サブフォルダのappX.exeを起動する処理を書く
    https://dobon.net/vb/dotnet/process/shell.html
    (相対パス指定も可能だが、ランチャー自身の実行パス情報を基準にした絶対パスに変換した方が確実)
    (作業ディレクトリを指定するにはProcessStartInfoで設定する必要あり)
    4. appX.exeを起動させたらランチャーは終了させれば、ユーザー視点ではショートカットとほぼ変わらない

    ソースコードの中身の部分はせいぜい10行前後くらいでしょうし、アイコンも自分で設定できます。

Your answer might help someone💌