Assembly.GetEntryAssembly について調べた

Assembly.GetEntryAssembly というメソッドがあります。これが何を返すか、即答できますか?
現在のプロセスの元になった exe ファイルのアセンブリでしょうか?

ちょっと疑問があったので調べてみました。

docs.microsoft.com

TL; DR

結論から言いますと、このメソッドはおそらく、現在の AppDomain 上で最初に実行されたエントリ ポイントを含むアセンブリを返します。

……🤔🤔🤔

アセンブリの種類

まず、アセンブリを 2 つに大別します。エントリ ポイントを持つアセンブリと、持たないアセンブリです。
平たく言えば、エントリ ポイントを持つアセンブリとは exe のことで、持たないアセンブリは dll のことです。
言わずもがな、エントリ ポイントとは、exe の Main メソッドのことです。

AppDomain でコードを実行する方法

AppDomain を指定してコードを実行する方法として、以下の 2 つがあります(他にもあったら教えてください)。

ExecuteAssembly は、エントリ ポイントを持つアセンブリを指定して実行します。
実行はエントリ ポイント、すなわち Main メソッドから始まります。Main メソッドの可視性(public / internal)は関係ありません。
Process.Start 等でプロセスを実行したときと似ていますが、呼び出し側のプロセス上で実行されるという点が異なります。

DoCallback は、任意のメソッドを指定して実行することができます。

既定の AppDomain

プロセスが実行されると、既定の AppDomain が作られます。
冒頭の記述を振り返りますと、「既定の AppDomain 上で最初に実行されたエントリ ポイントを含むアセンブリ」とは、言わずもがな、現在のプロセスの exe ファイルのアセンブリです。

従って、既定の AppDomain 上で実行する場合、ExecuteAssembly 経由であろうが DoCallback 経由であろうが、GetEntryAssembly は、現在のプロセスの exe ファイルのアセンブリを返します。

追加の AppDomain

.NET Framework では、AppDomain.CreateDomain で、新しい AppDomain を作ることができます(.NET Core では、現状、できません)。
このようにして作った AppDomain だと、ちょっと挙動が変わります。

もう一度、冒頭の記述を振り返りましょう。

現在の AppDomain 上で最初に実行されたエントリ ポイントを含むアセンブリを返します。

つまり、AppDomain は、最初に実行されたエントリ ポイントを含むアセンブリを記憶しています。

最初に ExecuteAssembly した場合

例えば、新規に作成した追加の AppDomain 上で、ExecuteAssembly を使用して、アセンブリ A を実行したとしましょう。すると、それ以降、この AppDomain 上では、ExecuteAssembly でアセンブリ B を実行しようが、DoCallback でアセンブリ C 上のコードを実行しようが、それらの中から呼んだ Assembly.GetEntryAssembly は A を返します。

実行のコール スタックとは関係ないことに注意してください。
ExecuteAssembly は、指定したアセンブリのエントリ ポイントを同期的に実行し、終了するまで制御を返しません。
ExecuteAssembly でアセンブリ A を実行し、その実行が完全に終わってから、ExecuteAssembly でアセンブリ B を実行しても、B 上の GetEntryAssembly は A を返します。

最初に ExecuteAssembly していない場合

追加の AppDomain 上で、一度も ExecuteAssembly をしないうちに、DoCallback を呼ぶとどうなるでしょうか。
この場合、DoCallback 上で実行される GetEntryAssembly は null を返します。これはドキュメントに記述されていない挙動です。
追加の AppDomain は、まだ一度もエントリ ポイントを実行していないからです。

補足

GetEntryAssembly のドキュメントにも記載されていますが、このメソッドは、アンマネージド プロセスから実行されると null を返すようです。この場合の挙動は実験していません。

まとめ

追加の AppDomain を作成することなんて、.NET Framework でも滅多にあることではありません。
今後、.NET のメインが .NET Core に移り変わっていく中で、そうした機会はますます減っていくでしょう(.NET Core で追加の AppDomain の作成がサポートされることは、おそらく今後もないと思われます)。
とはいえ、.NET Standard でクラス ライブラリを作っている場合は要注意です。
.NET Framework アプリケーションに読み込まれた場合、そのアプリケーションがどういう状況下でライブラリを呼んでいるか予測できないので、ライブラリ中で呼び出した GetEntryAssembly メソッドが、必ずしも現在のプロセスの exe ファイルのアセンブリを返すとは限らないからです。
この記事で言いたかったことはそれだけです。

ライブラリ中で、現在のプロセスの exe ファイルの情報が欲しい場合は、Process.GetCurrentProcess().MainModule を使うとよいかもしれません(が、このメソッドなら安心して使えるのかどうかは、詳しく調べていません)。