上級プロフェッショナル向け:VB.NETでの「AssemblyName」と`Assembly.LoadFrom`によるアドイン機構:プラグイン型アーキテクチャの構築と動的機能拡張の実現
長年、巨大な基幹業務システムやVBA製ツールの乱立、そしてスパゲッティ化したVB 6.0レガシーの現代化(モダナイゼーション)の最前線を戦ってきた者なら、誰もが一度はこう痛感しているはずだ。
「コアロジックに手を入れず、新機能だけを安全に差し込みたい」と。
VB.NET(および.NET / .NET Core)における動的なアセンブリ読み込み機構は、単なる「お遊びのリフレクション」ではない。これは、肥大化したモノリス(一枚岩)アプリケーションを解体し、真に拡張性の高いエンタープライズ・アーキテクチャを構築するための極限の防壁である。
本稿では、`AssemblyName` と `Assembly.LoadFrom` を極限までチューニングし、メモリリークや型安全性の崩壊といった現場の罠を完全に回避しながら、プラグイン型アーキテクチャを実装する極意を解説する。
—
1. なぜ「プラグイン機構」が必要なのか?:レガシーの呪縛からの脱却
日本の製造業や金融、物流の現場でいまだに稼働する多くのVB.NETアプリケーションは、ボタンクリックのイベントハンドラに業務ロジックが直書きされ、新機能を追加するたびに全体を再コンパイル・再リリースしている。この開発スタイルは、テスト工数の爆発と、デプロイ時のリスクという致命的な技術的負債を生む。
プラグイン機構を導入するメリットは明確だ:
- 独立デプロイメント: 本体(Host)を停止・改修することなく、新しい機能DLLを特定のフォルダに配置するだけで即座に機能拡張が行える。
- 関心の分離: 共通インターフェイスさえ定義しておけば、外部ベンダーにプラグイン単位で開発を委託できる。
- メモリとリソースの動的制御: 必要に応じてアセンブリをロードし、不要になればアンロードのフックを考慮した設計が可能(※厳密なアンロードには `AssemblyLoadContext` の理解が不可欠)。
—
2. アーキテクチャの全体像:ホスト、インターフェイス、プラグイン
堅牢なアドイン機構の基本原則は一つ。「ホストはプラグインの実体を知ってはならない。知るのは『共通契約(インターフェイス)』のみである」。
これを実現するため、ソリューションは以下の3つのプロジェクト(またはアセンブリ)に分割する。
1. Host.exe (メインアプリケーション): プラグインをスキャンし、動的にロードして実行する。
2. Contracts.dll (共通インターフェイス): ホストとプラグインが共通して参照する「契約書」。
3. PluginA.dll (具象プラグイン): Contractsを実装した実際の機能モジュール。
—
3. 実装コード:極限まで洗練されたVB.NETコード
ここからは、現場で即座に応用できるプロダクション品質のコードを示す。
Step 1: 共通契約(Contracts)の定義
まずは、ホストとプラグインの共通言語となるインターフェイスを定義する。
‘ Project: Contracts (Class Library)
Namespace Enterprise.PluginFramework
‘ すべてのプラグインが実装すべき共通インターフェイス
Public Interface IBusinessPlugin
ReadOnly Property PluginName As String
ReadOnly Property Version As Version
Sub Execute(ByVal context As IPluginContext)
End Interface
‘ プラグインにホスト側の環境を安全に渡すためのコンテキスト
Public Interface IPluginContext
ReadOnly Property ApplicationName As String
Sub LogMessage(ByVal message As String)
End Interface
End Namespace
Step 2: プラグイン(Concrete Plugin)の実装
次に、上記 Contracts を参照する独立したDLLプロジェクトを作成する。
‘ Project: PluginA (Class Library)
Imports Enterprise.PluginFramework
Public Class TaxCalculatorPlugin
Implements IBusinessPlugin
Public ReadOnly Property PluginName As String Implements IBusinessPlugin.PluginName
Get
=”消費税自動計算アドイン (2026年度対応)”
End Get
End Property
Public ReadOnly Property Version As Version Implements IBusinessPlugin.Version
Get
Return New Version(1, 0, 0, 0)
End Get
End Property
Public Sub Execute(context As IPluginContext) Implements IBusinessPlugin.Execute
context.LogMessage($”{PluginName} が実行されました。”)
‘ ここに実際の複雑な業務ロジックを記述
Dim basePrice As Decimal = 10000D
Dim tax As Decimal = basePrice 0.1D
context.LogMessage($”計算結果: 基準額={basePrice}, 税額={tax}”)
End Sub
End Class
Step 3: ホスト側での `Assembly.LoadFrom` と `AssemblyName` による動的ロード
ここが本稿の核心である。指定されたディレクトリからDLLを走査し、`AssemblyName` でアセンブリの整合性を検証した上で、安全にインスタンス化する。
‘ Project: Host (Windows Forms / Console Application)
Imports System.IO
Imports System.Reflection
Imports Enterprise.PluginFramework
Public Class PluginManager
Private _loadedPlugins As New List(Of IBusinessPlugin)
”’
”’
Public Sub LoadPlugins(ByVal pluginDirectory As String)
If Not Directory.Exists(pluginDirectory) Then
Directory.CreateDirectory(pluginDirectory)
Return
End If
‘ プラグインフォルダ内のすべてのDLLを取得
Dim dllFiles As String() = Directory.GetFiles(pluginDirectory, “.dll”)
For Each filePath As String In dllFiles
Try
‘ 1. ファイルパスから直接AssemblyNameを取得(ファイルロックを最小限に抑える高度なアプローチ)
Dim asmName As AssemblyName = AssemblyName.GetAssemblyName(filePath)
Console.WriteLine($”[発見] アセンブリ識別子: {asmName.FullName}”)
‘ 2. Assembly.LoadFromによる動的読み込み
‘ 注意: LoadFromは依存関係も含めて解決を試みるため、Host側から見える位置にContracts.dllがあることが前提となる
Dim asm As Assembly = Assembly.LoadFrom(filePath)
‘ 3. リフレクションによる型スキャン (IBusinessPluginを実装し、抽象クラスでない具象型を探す)
Dim pluginTypes = asm.GetTypes().Where(
Function(t) Not t.IsAbstract AndAlso Not t.IsInterface AndAlso GetType(IBusinessPlugin).IsAssignableFrom(t)
)
For Each t As Type In pluginTypes
‘ 4. 動的インスタンス化 (Activator.CreateInstance)
Dim instance As IBusinessPlugin = CType(Activator.CreateInstance(t), IBusinessPlugin)
If instance IsNot Nothing Then
_loadedPlugins.Add(instance)
Console.WriteLine($”[成功] プラグインロード完了: {instance.PluginName} (v{instance.Version})”)
End If
(Next
Catch ex As ReflectionTypeLoadException
‘ プラグイン内部で依存関係のロードに失敗した場合の詳細なローダー例外をキャッチ
Console.WriteLine($”[致命的エラー] 型のロードに失敗しました ({filePath}):”)
For Each loaderEx In ex.LoaderExceptions
Console.WriteLine($” -> {loaderEx.Message}”)
Next
Catch ex As Exception
Console.WriteLine($”[エラー] プラグインの読み込みに失敗しました ({filePath}): {ex.Message}”)
End Try
Next
End Sub
”’
”’
Public Sub ExecuteAll(ByVal context As IPluginContext)
For Each plugin In _loadedPlugins
Try
plugin.Execute(context)
Catch ex As Exception
Console.WriteLine($”[実行時例外] プラグイン ‘{plugin.PluginName}’ の処理中にエラーが発生しました: {ex.Message}”)
End Try
Next
End Sub
End Class
—
4. 上級プロフェッショナルが知るべき「罠」とメモリ最適化の極意
ここまでのコードで基本は動く。しかし、真に「プロフェッショナル」を名乗るのであれば、以下の実運用上のリスクと制約を完全にコントロールできなければならない。
① `Assembly.LoadFrom` の罠とファイルロック
`Assembly.Load` と異なり、`Assembly.LoadFrom` は指定されたファイルパスからアセンブリを読み込むため、アプリケーションが実行されている間、該当するDLLファイルがOSによってロックされる。
運用中に「新しいバージョンのプラグインDLLに差し替えたい」という場合、ホストプロセスを一度終了させなければならない。
- 解決策の方向性:
厳密なホット・スワップ(無停止でのプラグイン差し替え)が求められる環境では、`Assembly.LoadFrom` ではなく、ファイルを一度メモリ(Byte配列)に読み込んでから `Assembly.Load(Byte())` を経由するか、.NET Core以降であれば `AssemblyLoadContext` をカスタマイズして、アセンブリ単位でのアンロード(Unload)を明示的に実装する必要がある。
② 型のミスマッチとバージョン地獄(Assembly Identity)
ホストが参照している `Contracts.dll` のバージョンと、プラグインがビルド時に参照した `Contracts.dll` のバージョンが微妙に異なる場合、`.NET` は型のキャスト時に `InvalidCastException` を発生させる。
これを防ぐため、共通契約アセンブリは厳密な署名(Strong Name)を行うか、インターフェイスの変更時は必ずセマンティックバージョニングに従い、ABI(Application Binary Interface)の互換性を維持する強固なガバナンスが不可欠である。
③ COM相互運用・Windows API連携時の注意
レガシーシステムとの統合において、プラグイン内部からWin32 APIやCOMオブジェクトを操作する場合、スレッドモデル(STA / MTA)の不一致によるクラッシュ(RPC_E_WRONG_THREADなど)が頻発する。
ホスト側で `Thread.SetApartmentState` を適切に制御しているか、プラグインのエントリポイントでスレッドコンテキストが保証されているかを常に意識せよ。
—
5. 結び:コードの寿命を延ばすために
VB.NETは「古い言語」ではない。言語の背後にあるCLR(共通言語ランタイム)の本質、そしてアセンブリのライフサイクルを理解する者にとって、極めて強力で実用的なツールであり続ける。
「本体を改修しない」という制約は、システムに究極の保守性と美しさをもたらす。今日紹介したパターンをあなたのプロジェクトのアーキテクチャに組み込み、レガシーの呪縛を完全に断ち切ってほしい。
