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を開発するチームがアプリケーションを開発します。

今回のソフトウェアの構成はこんな感じです。

f:id:jyuch:20160131220352p:plain

今回のシナリオではMyUtil ver.1.0MyUtilWrapperAMyUtil ver.1.1MyUtilWrapperBをそれぞれ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

この場合、MyUtilWrapperAMyUtilWrapperBMyAppを再コンパイルすることなく、アプリケーション構成ファイルを書き換えるだけで使用するバージョンを切り替えることができます。

<?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>も存在しない場合はプロービングでアセンブリが検索されるとか、特定のディレクトリをプロービング対象にするためにはどうするかとか、発行者ポリシーでもリダイレクトが出来たりとか、発行者ポリシーを意図的に無視したりとかがありますがこの辺は用語だけの紹介とします。

github.com

おわり

*1:厳密に言えば違うのですが、プロービングでアセンブリを検索する場合はファイル名とアセンブリ名が一致してないと正しく検索できないのと、コンパイラアセンブリリンカはファイル名と同じアセンブリ名を付けるのでファイル名とアセンブリ名を一致させる必要があります