3. .NET アセンブリのバージョン管理

更新日: 2009 年 10 月 23 日


.NET のアセンブリでは、.NET 以前に頻発していた DLL Hell 問題を回避するために、アセンブリのバージョン管理を厳密に行えるようになっています。

.NET 以前の問題点

.NET が普及する以前は、COM (Component Object Mode) と呼ばれるコンポーネントによりアプリケーションを構成するのが一般的な形式でした。COM コンポーネントでは、レジストリにその情報を登録しておき、コンポーネントの呼び出しが必要になった時点でレジストリ情報を参照し、コンポーネントを読み込むという処理が行われていました。

例えば、Excel コンポーネントを利用する Visual Basic 6.0 のコードを見てみましょう。

Dim xl As Object
Set xl = CreateObject("Excel.Application")

このプログラムが実行されると、内部ではレジストリから Excel.Application に関する情報を参照して、ライブラリを読み込むという処理が行われます。

このプログラムが実行されると、内部ではレジストリから Excel.Application に関する情報を参照して、ライブラリを読み込むという処理が行われます。

この処理自体は、Excel 2003 でも、Excel 2007 でも実施することができます。しかし、もしアプリケーションが使用している Excel 2003 コンポーネントの処理が、Excel 2007 で変更され、互換性がなくなってしまったとしたらどうでしょう? Excel 2007 をインストールした時点で、これまで正常に動作していたアプリケーションが正しく動作しなくなってしまいます。

Excel などのアプリケーションの場合はまだ問題が明確ですが、共通的に使用されているコンポーネント (DLL ファイル) が何かのアプリケーションをインストールしたときや修正パッチを適用したときに入れ替わってしまった、いうのはよくあることです。

Excel などのアプリケーションの場合はまだ問題が明確ですが、共通的に使用されているコンポーネント (DLL ファイル) が何かのアプリケーションをインストールしたときや修正パッチを適用したときに入れ替わってしまった、いうのはよくあることです。

Windows のファイル管理の仕組みでは、共通的な DLL ファイルは、Windows フォルダー配下の System32 フォルダーで管理することになります。そのため、アプリ 1 とアプリ 2 で共通的に使用されていた DLL ファイルがアプリ 2 のバージョンアップで置き換えられてしまった場合、アプリ 1 が正常に動作しなくなってしまう可能性があります。

.NET ではこれらの問題を回避するために、アプリケーションやコンポーネントの厳密なバージョン管理が行えるようになっています。

ページのトップへ


バージョン番号

.NET では、アセンブリを識別する 1 つの要素としてバージョン番号を使用します。DLL Hell 問題を回避するためにもバージョン番号を正しく使用する必要があります。

通常、バージョン番号には、ピリオドで区切られた 4 つの数値が使用されます。具体的には、「1.2.369.5」のような番号になります。数値の意味はそれぞれ左から、メジャー番号、マイナ番号、ビルド番号、リビジョン番号です。

開発時には、バージョン番号はきちんとした規約を作成してアセンブリの競合が発生しないように管理します。通常は、開発前にメジャー番号、マイナ番号を決定します。その後、開発を進めていく中で、日次ビルドなどでビルド番号を設定し、各ビルドで発生した問題を解決してリビジョンをアップしたときにリビジョン番号をインクリメントします。

アセンブリにバージョン番号を付けるためには、ソースの先頭部分に以下のような記述を行います。

using System.Reflection;
[assembly:AssemblyVersion("1.0.0.0")]
[assembly:AssemblyInformationalVersion("1.0.0.0")]
[assembly:AssemblyFileVersion("1.0.0.0")]

このようなソースをビルドしてアセンブリ (EXE ファイルや DLL ファイル) を生成すると、以下のように エクスプローラー からも、そのバージョンが確認できます。

このようなソースをビルドしてアセンブリ (EXE ファイルや DLL ファイル) を生成すると、以下のように エクスプローラ からも、そのバージョンが確認できます。

上記のコードでは、.NET で使用可能な 3 種類のバージョン番号を属性により指定していますが、このうち .NET のバージョン管理で重要なのは AssemblyVersion (アセンブリ バージョン) になります。それぞれのバージョン番号の役割は以下のようになっています。

バージョン番号 (属性名) 説明
AssemblyVersion .NET がアセンブリのバージョン管理で使用するバージョン。このバージョンが同じアセンブリは、同じものだと判断されます。
AssemblyInformationalVersion プログラムのバージョン。アセンブリに含まれるプログラムのバージョンを表示するために使用します。
AssemblyFileVersion Win32 ファイルシステムで使用されるファイル バージョン。ユーザーがファイルのバージョンを確認したいときに使用します。

ページのトップへ


厳密名

.NET で DLL Hell 問題を完全に解消するためには、バージョン管理に加え、厳密名と呼ばれる名前をアセンブリに付ける必要があります。

厳密名には、「簡易名」「バージョン情報」「カルチャ」「公開キー」といった情報が含まれます。厳密名が付けられていない場合のアセンブリの名前は「簡易名」と呼ばれます。簡易名は、通常、出力されたアセンブリのファイル名から拡張子を除いたものが自動的に付けられます。簡易名のみのアセンブリでは、アセンブリを上書きするだけでプログラムの更新が完了するというメリットがありますが、厳密なバージョン管理ができない、同じ名前のアセンブリにより乗っ取られてしまう可能性がある、などの問題があります。

.NET 以前の COM では、クラス ID と呼ばれる一意の ID を使って、コンポーネントが識別されていました。例えば、Excel の機能を別のプログラムから扱う Excel.Application という COM コンポーネントには、{00024500-0000-0000-C000-000000000046} という ID が振られています。この ID は、世界中で一意になるように設計されている GUID (Globally Unique Identifier) と呼ばれる ID です。このため、例えば、「Excel.Application」という同じ名前のコンポーネントが存在していたとしてもクラス ID が異なるためコンポーネントは一意に識別することができます。ただし、クラス ID は公開されている情報のため、悪意を持った開発者が同じ ID のオブジェクトを作成してしまうと利用者の意図しないコンポーネントが読み込まれて、アプリケーションが乗っ取られてしまう可能性もありました。

.NET の厳密名には、アセンブリを厳密に特定するために公開キーが付与されています。この公開キーは、アセンブリの開発者が開発時に RSA デジタル署名を行うのに使用した秘密キーとペアになっていたキーです。アセンブリに付与されている公開キーを使うと、そのアセンブリが署名されたときから改ざんされていないことを確認できます。つまり、実行時に参照するアセンブリが、開発時に参照しているアセンブリと同一であることが保証されるわけです。もちろん、秘密キーはアセンブリの開発が完了した後も、開発者が厳重に保管しておく必要があります。

アセンブリに署名するには、秘密キーと公開キーのセットであるキーペアをまず用意する必要があります。キーペアの作成には、厳密名ツール (Sn.exe) コマンドで次のようにして行えます。

sn -k pair.snk

これによりキーペアが作成され、pair.snk ファイルに出力されます。このようにして作成したキーペアを使用してアセンブリに署名します。これには、ソース ファイルのコンパイル時に /keyfile オプションでキーペア ファイルを指定します。

csc /t:library /keyfile:pair.snk lib.cs

このようにしてライブラリとなるコンポーネントに署名して、厳密名を付けることができます。もちろん、この作業は Visual Studio 上でも GUI により簡単に行うことができます。

次に、アセンブリにバージョン情報を設定する方法ですが、そのアセンブリに含まれるソースコードに、以下のように AssemblyVersion 属性を使用してアセンブリのバージョンを記述します。Visual Studio を使用して開発を行っている場合は、AssemblyInfo.cs というアセンブリ情報が記述されたファイルが自動的に作られて、バージョン情報はそこに記述されています。

[assembly:AssemblyVersion("1.0.0.0")]

このようにバージョン情報を指定してビルドすることによって、アセンブリにバージョン情報が設定されます。

最後にカルチャの設定ですが、カルチャは一般的に文字列情報などの言語や文化に依存したリソースで使用します。通常、アプリケーションを複数の言語に対応する場合、コードを含むアセンブリは共通で使用するために、カルチャを言語非依存の「Neutral」にしておきます。そして、メッセージなどの文字列情報を含む外部リソースを別のアセンブリとして構成し、特定のカルチャを設定します。このように、カルチャに依存したリソース ファイルで構成されるアセンブリをサテライト アセンブリと呼びます。

アセンブリにカルチャを指定する場合は、アセンブリを生成するソース コードに次のように AssemblyCulture 属性を指定します。

[assembly:AssemblyCulture("ja-jp")]

ja-jp は、「言語と国 (地域) 」が「日本語と日本」であることを表しています。

厳密名の例として、.NET Framework 2.0 (バージョン 2.0) の System.Xml アセンブリ (System.XML.dll) の厳密名を見てみましょう。この厳密名は具体的には次のようになっています。

System.Xml, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=b77a5c561934e089

悪意を持った第三者が System.Xml.dll と同じクラスやメソッドを持つアセンブリを作成して、アプリケーションを乗っ取ろうと考えたとしましょう。バージョンやカルチャまでは問題なく同じにすることができますが、秘密キーを入手する手段がないため、公開キーを同じにすることができず、結果としてアプリケーションの乗っ取りは失敗します。このように、アセンブリに厳密名を設定することで、アプリケーションは出所もバージョンも正しいアセンブリを参照して実行することができるようになっています。

ページのトップへ


GAC (Global Assembly Cache)

.NET では、GAC (Global Assembly Cache) と呼ばれるコンピューターに共通的なアセンブリの管理フォルダーが提供されています。Windows でいう System32 フォルダーのようなものですが、バージョンやアセンブリの管理が厳密に行える点が異なります。System32 フォルダーでは、単純にファイル単位で共有ライブラリが管理されているため、同じファイル名の異なるバージョンのライブラリを管理できません。また、不正なライブラリで上書きされてしまう可能性がありました。

GAC では、同じファイル名のアセンブリでも複数のバージョンが管理できるようになっているため、より安全に共有ファイルの管理を行うことができます。GAC は、Windows フォルダー配下の assembly フォルダーに存在しています。

実際にそのフォルダーを開いてみると、以下のイメージのように同じ名前でバージョンが異なるアセンブリが複数管理されていることが分かります。このような表示形式は、エクスプローラーのシェル拡張機能であるアセンブリ キャッシュ ビューアー (shfusion.dll) によって実現されています。

このような表示形式は、エクスプローラのシェル拡張機能であるアセンブリ キャッシュ ビューア (shfusion.dll) によって実現されています。

ここからも、厳密名で設定した情報がアセンブリを一意に識別する ID になっていることが分かります。

エクスプローラーではこのように見えますが、コマンド プロンプトを使って GAC の中を見ると、生のフォルダー構成をのぞき見ることができます。

エクスプローラではこのように見えますが、コマンド プロンプトを使って GAC の中を見ると、生のフォルダ構成をのぞき見ることができます。

assembly フォルダーの中には、「GAC」で始まるフォルダーが 3 つあるのが分かります。64 ビット環境では、これに加えて GAC_64 フォルダーが作成されます。temp と tmp フォルダーは一時フォルダーのため無視するとして、それ以外のフォルダーは次のような用途で使用されています。

フォルダー名 用途
GAC CLR 1.0、1.1 で作成されたアセンブリが登録されています。
GAC_MSIL CLR 2.0 で作成されたアセンブリが登録されています。マネージ コードだけで構成されているアセンブリが登録されます。
GAC_32 CLR 2.0 で作成されたアセンブリが登録されています。これらは 32 ビット ネイティブ コードを含むか、32 ビット環境を前提として作成されています。
GAC_64 CLR 2.0 で作成されたアセンブリが登録されています。これらは 64 ビット ネイティブ コードを含むか、64 ビット環境を前提として作成されています。

また、「NativeImages」で始まるフォルダーには MSIL をネイティブ コードにコンパイルした結果がキャッシュされています。

フォルダー構成はどのフォルダーも同じなので、ここでは GAC_MSIL 以下を見てみましょう。Dir コマンドを使ってフォルダーの中を見てみると、エクスプローラーで見たアセンブリが実際にはフォルダーで管理されているのが分かります。

Dir コマンドを使ってフォルダの中を見てみると、エクスプローラで見たアセンブリが実際にはフォルダで管理されているのが分かります。

ここで、System.Xml フォルダーに移動してみましょう。

ここで、System.Xml フォルダに移動してみましょう。

カルチャが明示されていないため分かりにくいかもしれませんが、

<バージョン番号>_<カルチャ>_<公開キー トークン>

という名前のフォルダーが作成されているのが分かります。さらにこのフォルダーの中を見てみるとアセンブリ本体である System.XML.dll を見つけることができます。

さらにこのフォルダの中を見てみるとアセンブリ本体である System.XML.dll を見つけることができます。

つまり GAC では、簡易名のフォルダーの下に、厳密名の各要素をアンダーバー (_) で区切った名前のフォルダーを作成してアセンブリを管理しているため、同じ名前のファイルであっても、バージョン番号や公開キーが異なる場合には、異なるものとして管理できるようになっています。

以上のように、アセンブリを GAC で管理することによって、DLL Hell 問題を発生させることなく、アセンブリの複数バージョンを同時に配置できます。また、もし開発した複数のバージョンのアセンブリを管理する必要があれば、アセンブリに厳密名を付けてエクスプローラーでアセンブリを GAC フォルダーにドラッグ & ドロップするだけで、上記のようなフォルダーが構成されてアセンブリを管理できます。

ページのトップへ