読者です 読者をやめる 読者になる 読者になる

銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

AWS Tools for Windows PowerShell と SDK for .NET を参照するCmdletを組み合わせたらMissingMethodExceptionが出た話

あまりこの現象に遭遇することはないと思いますが、遭遇してそれなりに原因究明に時間がかかったのでまとめておこうと思います。

AWSにはWindows環境でAPIを実行するツールとして、Tools for PowerShell と SDK for .NET があります。

AWS Tools for Windows PowerShell

AWS SDK for .NET | アマゾン ウェブ サービス(AWS 日本語)

さて、SDK for .NET を参照してC#で作成したPowerShell のCmdletバイナリを作った上で、PowerShellスクリプトの中で Tools for PowerShell と作成したCmdletの両方をインポートしてCmdletで定義したコマンドを実行していました。すると、なんということでしょう!MissingMethodExceptionが出るようになってしまったのです。

MissingMethodExceptionというのは、つまりビルドしたときの参照DLLと実行時の参照DLLが異なっているから起きているわけですが、そのCmdletはビルドしてできたDLL群をそのまま配置して実行していたため、最初は「???」という状態でした。

いろいろ調べていくうちに原因がわかりました。

AWS Tools for Windows PowerShell は AWS SDK for .NET の.NET 3.5版と思われるバイナリを参照する

AWS Tools for Windows PowerShell はインストーラーとして提供されていますが、インストールするとC:\Program Files (x86)\AWS Tools\PowerShell\AWSPowerShell\以下に参照するDLL群を配置します。このDLLなんですが、どうやらAWS SDK for .NET と同じアセンブリ名をつけており、AssemblyDescriptionを見るに.NET 3.5向けのバイナリのようです*1

f:id:tanaka733:20151211201524j:plain

コード*2

var assembly = Assembly.LoadFrom(@"D:\tmp\AWSSDK.ElasticLoadBalancing-toolsforps.dll");
assembly.FullName.Dump();
assembly.GetCustomAttributes(typeof(AssemblyDescriptionAttribute), false)
        .Cast<AssemblyDescriptionAttribute>().FirstOrDefault()?.Description.Dump();

AWS SDK for .NET は .NET 4.5を参照している

これはCmdletクラスを作成するときのTarget Frameworkが.NET 4.5以上にしていますし、実行環境も.NET 4.5以上なのでまあ当たり前です。で、こちらのDLLの情報見るとアセンブリバージョンが完全に一致していることがわかります。

f:id:tanaka733:20151211201825j:plain

.NET 3.5 では(当然のごとく)Async系のメソッドが定義されていない

まあ、これはそうですよね。ソースコードレベルでメソッドが定義されていないか確認できればいいのですが、Tools for PowerShellの方はコードが提供されていないので*3実際に参照してコードを書いてみるとコード補完されないことがわかるでしょう。

つまり混ぜるな危険

SDK for .NET を参照して作ったCmdletクラスが.NET 4.5以上向けであって、Async系のメソッドを利用している場合を考えます。

  1. Cmdletは普通にビルドできて配置できる
  2. Imrpot-Module AWSPowerShellすると.NET 3.5向けのDLLが先に読み込まれる
  3. Import-Module MyCmdlet.dlllすると*4、AWS SDK のアセンブリをロードしようとするが、先に同じ名前*5で.NET 3.5向けのDLLが読み込まれているため、.NET 4.5向けのDLLは読み込まれない
  4. MyCmdletの中で.NET 4.5向けでしか存在しないメソッド(Async系など)を呼び出している部分を実行しようとしたとき、実際にそのときに読み込まれているDLLは.NET 3.5向けのDLLなのでメソッドが存在せずMissingMethodExceptionがスローされる

という流れで起きていました。

対策は?

一時しのぎの対策はいくつかあると思うんですが、本質的な対策をどうするか?というのは割と悩んでいます。例えば Tools for PowerShell が参照するDLLのアセンブリ名がSDK for .NET と違えば問題ないんですが、本当に SDK for .NET の.NET 3.5向けのアセンブリをそのまま利用しているのであれば、そのアセンブリ名を変更してほしいというのもどうだろう?という気がします*6

かといって、今更Cmdletを.NET 3.5に落として書くのも苦痛です*7

きれいな対策はまだ思いついていないのですが、もし同じ問題が起きている人がいたら参考になればと思いまとめておきました。

おまけ

このブログは結果が分かってから書いたので、この順番になっていますが、実際原因探っているときはまったくわけわからん状態でした。で、MissingExceptionが出ているということはきっと参照しているDLLが想定しているものとは違うはずだ!ということでこのコードを埋め込んで、DLLがTools for PowerShell由来であることを確かめました、

var assembly = typeof(AmazonElasticLoadBalancingClient).Assembly;
Console.WriteLine($"{assembly.Location} {assembly.FullName}");

*1:たしかにTools for Windows PowerShellの動作要件は.NET 3.5以上

*2:DLLを移動してリネームしてあります

*3:本当に.SDK for .NETの.NET3.5向けバイナリなのであれば、GitHubで提供されています

*4:MyCmdletは作成したCmdletのバイナリ

*5:バージョンも同じである必要がありますが、どちらも安定最新版を入れているとバージョンも同じになります

*6:Nugetの仕組みとして、.NET 4.5以上とそれ未満に分けて同じアセンブリ名で別のバイナリを提供できるようにそもそもなっているので

*7:それなりに量のあるコード