VB.NETでもside-by-side実行をしたい
はじめに
はじめに一つ釈明といいますか謝罪的なアレなんですが、タイトルにVB.NETと冠していますが正直VBほとんど関係ありません。 サンプルコードにVBを使ってるだけで正直C#でもVBでも大して変わりません。
side-by-side実行とGAC(グローバル・アセンブリ・キャッシュ)
side-by-sideを試す前に注意しておいて欲しいことがあります。
サイドバイサイド実行とは、文字通り実行時の話で、インストール時の話ではない点に注意してほしい。サイドバイサイド実行とは、「GAC(グローバル・アセンブリ・キャッシュ)に複数のバージョンをインストールできること」ではないし、「複数のバージョンが共存できること」でもない。コンパイル時とまったく同じアセンブリを使い続けることを意味する。
インサイド .NET Framework 第4回 アセンブリとバージョン管理 - 吉松 史彰
ということです。side-by-sideというとどうしても複数バージョンの話が出てきてそれで「CACに複数バージョンのアセンブリをインストール出来る」みたいな感じで勘違いをし易いのですが、あくまでもGACにインストールするときに同じファイル名が上書きされると困るのでGACはアセンブリの厳密名をもって管理を行うってだけです。
正直弊社も最初の方は勘違いをしていたので一応正しく補足するとって感じで。
厳密名を持つアセンブリ
.NETにおいてside-by-side実行を行うには、ロードされるアセンブリが厳密名を持つアセンブリである必要があります。
厳密名を持つアセンブリは
- ファイル名(拡張子を除く)*1
- バージョン
- カルチャID
- 公開キー
で識別されます。どれか一つでも違うと異なるアセンブリとして認識されます。
また、アセンブリに厳密名を付与すると
- バージョン管理を行える(side-by-side実行)
- アセンブリの改ざんへの耐性を持たせられる
などのメリットがあります。
厳密名を持つアセンブリを作成するためにはアセンブリをキーペアを用いてデジタル署名する必要があります。 具体的な方法については以下のセクションで紹介します。
猫でもわかるside-by-side
キー・メーカー
とりあえずキーペアでも作成しておきますか。
と言うことでDeveloper Command Prompt for VS2015
あたりのツールへのパスが通っているプロンプトで以下のコマンドを入れてキーペアを作成しておきます。
sn -k Jyuch.snk
シナリオ
ここで今回の開発のシナリオを軽く説明します。
まず、ソフトウェア全体で使用される基底ライブラリMyUtil
を担当するチームがライブラリを開発します。
次に基底ライブラリを使用してそれぞれ適用範囲が異なるライブラリMyUtilWrapperA
及びMyUtilWrapperB
を作成するチームがそれぞれライブラリを開発します。
最後にこれらの成果物を使用して最終的な成果物であるMyApp
を開発するチームがアプリケーションを開発します。
今回のソフトウェアの構成はこんな感じです。
今回のシナリオではMyUtil ver.1.0
とMyUtilWrapperA
、MyUtil ver.1.1
とMyUtilWrapperB
をそれぞれside-by-sideさせます。
ついでにディレクトリ構造も紹介しておきましょう。
C:\PROJECTS\SIDEBYSIDE │ Jyuch.snk │ MyApp.exe │ MyApp.exe.config │ MyApp.vb │ ├─MyUtil.v.1.0 │ MyUtil.dll │ MyUtil.vb │ ├─MyUtil.v.1.1 │ MyUtil.dll │ MyUtil.vb │ ├─MyUtil.v.1.2 │ MyUtil.dll │ MyUtil.vb │ ├─MyUtilWrapperA │ MyUtilWrapperA.dll │ MyUtilWrapperA.vb │ └─MyUtilWrapperB MyUtilWrapperB.dll MyUtilWrapperB.vb
実装
まず、基底ライブラリのMyUtil
を実装します。
とりあえず自身のバージョンを出力する役に立たないライブラリをポンと実装。
Imports System.Reflection ' アセンブリバージョン <Assembly: AssemblyVersion("1.0.0.0")> Namespace MyUtil Public Class Util Public ReadOnly Property Version As String Get Return "1.0" End Get End Property End Class End Namespace
今回は珍しくVisual Studioを使わないでいきます。 と言うことでプロンプトからコンパイラをポン。
vbc /target:library /verbose /keyfile:../Jyuch.snk MyUtil.vb
次にライブラリのラッパーライブラリを実装します。 と言うことでほい。
Imports System.Reflection Imports MyUtil ' アセンブリバージョン <Assembly: AssemblyVersion("1.0.0.0")> Namespace MyUtilWrapperA Public Class UtilWrapper Private _internalUtil As Util Public Sub New() _internalUtil = New Util() End Sub Public Function GetInternalUtilVersion As String Return _internalUtil.Version End Function End Class End Namespace
こちらもなんとなく厳密名を付けておきましょう。 というわけでコンパイル。
vbc /target:library /verbose /keyfile:../Jyuch.snk /reference:../MyUtil.v.1.0/MyUtil.dll MyUtilWrapperA.vb
と、ここでMyUtil
の開発チームがプロダクトにバグを発見したようで、そのバグを修正したバージョン1.1をリリースしました。
Imports System.Reflection ' アセンブリバージョン <Assembly: AssemblyVersion("1.1.0.0")> Namespace MyUtil Public Class Util Public ReadOnly Property Version As String Get Return "1.1" End Get End Property End Class End Namespace
vbc /target:library /verbose /keyfile:../Jyuch.snk MyUtil.vb
MyUtilWrapperB
開発チームはバグが修正されたMyUtil
のバージョン1.1を使用することを決め、自身のプロダクトをリリースしました。
Imports System.Reflection Imports MyUtil ' アセンブリバージョン <Assembly: AssemblyVersion("1.0.0.0")> Namespace MyUtilWrapperB Public Class UtilWrapper Private _internalUtil As Util Public Sub New() _internalUtil = New Util() End Sub Public Function GetInternalUtilVersion As String Return _internalUtil.Version End Function End Class End Namespace
vbc /target:library /verbose /keyfile:../Jyuch.snk /reference:../MyUtil.v.1.1/MyUtil.dll MyUtilWrapperB.vb
ところがMyUtilWrapperA
の開発チームは互換性テストを怠りMyUtil
のバージョン1.0を使用し続けることを決定しました。クソだね。
ライブラリの開発が完了したのでMyApp
チームは最終的な成果物であるアプリケーションを実装しました。
Imports System.Reflection Imports WrapperA = MyUtilWrapperA.UtilWrapper Imports WrapperB = MyUtilWrapperB.UtilWrapper ' アセンブリバージョン <Assembly: AssemblyVersion("1.0.0.0")> Namespace MyApp Public Class Program Public Shared Sub Main(args As String()) Dim a = New WrapperA() Dim b = New WrapperB() Console.WriteLine($"WrapperA: {a.GetInternalUtilVersion}") Console.WriteLine($"WrapperB: {b.GetInternalUtilVersion}") End Sub End Class End Namespace
vbc /target:exe /verbose /keyfile:Jyuch.snk /reference:./MyUtilWrapperA/MyUtilWrapperA.dll,./MyUtilWrapperB/MyUtilWrapperB.dll MyApp.vb
最後に厳密名を持つアセンブリの場所をアプリケーション構成ファイルを用いてMyApp
(を実行するCLR)に伝えます。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="MyUtil" publicKeyToken="daaeb4fcc5fa526b" culture="neutral" /> <codeBase version="1.0.0.0" href="file:///C:/Projects/SideBySide/MyUtil.v.1.0/MyUtil.dll" /> <codeBase version="1.1.0.0" href="file:///C:/Projects/SideBySide/MyUtil.v.1.1/MyUtil.dll" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="MyUtilWrapperA" publicKeyToken="daaeb4fcc5fa526b" culture="neutral" /> <codeBase version="1.0.0.0" href="file:///C:/Projects/SideBySide/MyUtilWrapperA/MyUtilWrapperA.dll" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="MyUtilWrapperB" publicKeyToken="daaeb4fcc5fa526b" culture="neutral" /> <codeBase version="1.0.0.0" href="file:///C:/Projects/SideBySide/MyUtilWrapperB/MyUtilWrapperB.dll" /> </dependentAssembly> </assemblyBinding> </runtime> </configuration>
それで実行結果がこれ。
WrapperA: 1.0 WrapperB: 1.1
ちゃんとside-by-sideされていますね。やったぜ
アセンブリ・リダイレクト
ここに追加のシナリオを考えます。
MyUtil
開発チームがバージョン1.1にバグを見つけ、修正したバージョン1.2をリリースしました。
割と致命的なバグだったためMyApp
の開発チームは新しいバージョンを使用したいと考え、MyUtilWrapperA
およびMyUtilWrapperB
開発チームに互換性の確認を取らせた上でMyUtil
アセンブリをバージョン1.2を使用することにしました。
Imports System.Reflection ' アセンブリバージョン <Assembly: AssemblyVersion("1.2.0.0")> Namespace MyUtil Public Class Util Public ReadOnly Property Version As String Get Return "1.2" End Get End Property End Class End Namespace
この場合、MyUtilWrapperA
・MyUtilWrapperB
・MyApp
を再コンパイルすることなく、アプリケーション構成ファイルを書き換えるだけで使用するバージョンを切り替えることができます。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="MyUtil" publicKeyToken="daaeb4fcc5fa526b" culture="neutral" /> <!-- <codeBase version="1.0.0.0" href="file:///C:/Projects/SideBySide/MyUtil.v.1.0/MyUtil.dll" /> --> <!-- <codeBase version="1.1.0.0" href="file:///C:/Projects/SideBySide/MyUtil.v.1.1/MyUtil.dll" /> --> <codeBase version="1.2.0.0" href="file:///C:/Projects/SideBySide/MyUtil.v.1.2/MyUtil.dll" /> <bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="1.2.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="MyUtilWrapperA" publicKeyToken="daaeb4fcc5fa526b" culture="neutral" /> <codeBase version="1.0.0.0" href="file:///C:/Projects/SideBySide/MyUtilWrapperA/MyUtilWrapperA.dll" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="MyUtilWrapperB" publicKeyToken="daaeb4fcc5fa526b" culture="neutral" /> <codeBase version="1.0.0.0" href="file:///C:/Projects/SideBySide/MyUtilWrapperB/MyUtilWrapperB.dll" /> </dependentAssembly> </assemblyBinding> </runtime> </configuration>
それで実行結果がこれ。
WrapperA: 1.2 WrapperB: 1.2
ちゃんと1.2にリダイレクト出来ています。
おわりに
よくわからんストーリーテリングに乗っけてside-by-side実行をさせてみました。
バグや仕様外の挙動を意図的に利用してしまうとそのバグや内部動作が修正された新しいバージョンを使用すると既存のソフトウェアが動かなくなるとかあるらしく、それを回避させるためにもside-by-sideが使われるそうで。
また、ライブラリがGACにインストールされている場合は構成ファイルの<codeBase>
を設定をしなくてもokだとか、GACにインストールするには厳密名を持つアセンブリじゃないとダメだとか、GACにインストールされていなくて<codeBase>
も存在しない場合はプロービングでアセンブリが検索されるとか、特定のディレクトリをプロービング対象にするためにはどうするかとか、発行者ポリシーでもリダイレクトが出来たりとか、発行者ポリシーを意図的に無視したりとかがありますがこの辺は用語だけの紹介とします。
おわり