【テクニカル・上級編】VB.NETによるリフレクションとAppDomainを活用したプラグイン機構の自社開発:コアアプリケーションを変更せずに外部DLLから新機能・帳票レイアウトを動的ロードするアーキテクチャ – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

【VB.NET極限開発】AppDomainとリフレクションによる、プラグイン機構の完全実装アーキテクチャ

レガシーシステムの寿命を延ばし、ビジネスの狂気的なまでの仕様変更スピードに対応するためには、「コアを変更せずに機能拡張を無限に後付けできる構造」が不可欠だ。

特に日本の製造業や金融・流通の基幹システムを支えるWindows Formsアプリケーションにおいて、帳票レイアウトの追加や外部連携モジュールの差し替えのたびに全リビルドとデプロイを行っているようでは、アーキテクトとしての名がすたる。

今回は、VB.NETの真骨頂である動的アセンブリロード、厳格なインターフェイス契約、そして.NET FrameworkにおけるAppDomain(アプリケーションドメイン)を駆使した「安全なアンロード」を含む、エンタープライズ向けプラグイン機構の全貌を解説する。

中途半端な解説はしない。現場で明日から使えるレベルの実装コードと共に、メモリ管理の罠や型安全性の担保といった実務の急所を余すところなく暴く。

—

1. なぜ「AppDomain」と「インターフェイス」が必要なのか

VB.NETでプラグインを実装する際、単に `Assembly.LoadFrom` を使ってDLLを読み込むだけなら素人でも書ける。しかし、それでは致命的な問題に直面する。

  • DLLのロック問題: .NETにおいて、一度ロードされたアセンブリはデフォルトのAppDomainからアンロードできない。つまり、アプリケーションを再起動しなければプラグインの差し替え(バージョンアップ)ができない。
  • 型の不一致と依存関係: コアとプラグインが同じインターフェイス定義を共有していなければ、ポリモーフィズムによる安全な呼び出しができない。

これを解決するのが、「分離されたAppDomainの生成」と「MarshalByRefObjectの継承」、そして「共通コントラクト(インターフェイス)アセンブリ」の3つを組み合わせたアーキテクチャである。

—

2. アーキテクチャの全体像

1. Contract(共通インターフェイス層): コアとプラグイン的双方が参照する共通の契約。
2. Core(ホストアプリケーション): メインのWindows Formsアプリ。プラグインの検出、専用AppDomainへのロード、実行、そして破棄を統括する。
3. Plugin(外部DLL): コアを知らず、Contractだけを知る独立したアセンブリ。

—

3. 実装コード:妥協なきエンタープライズ・プラグイン基盤

① Contract アセンブリ(共通契約)

まずは、コアとプラグインを繋ぐ唯一の契りを定義する。このアセンブリは変更してはならない(バージョン厳守)。

‘Namespace: Enterprise.Plugin.Contract
Imports System


Public Enum PluginExecutionResult
Success = 0
Warning = 1
[Error] = 2
End Enum

”’

”’ すべてのプラグインが実装すべき基本インターフェイス
”’

Public Interface IEnterprisePlugin
ReadOnly Property PluginName As String
ReadOnly Property Version As Version

‘ 初期化処理
Sub Initialize(ByVal hostContext As Object)

‘ メイン実行処理(帳票出力やデータ処理)
Function Execute(ByVal parameters As System.Collections.Generic.Dictionary(Of String, String)) As PluginExecutionResult
End Interface

② Core アセンブリ(ホスト側の制御エンジン)

ここからが本番だ。別AppDomain上でプラグインをインスタンス化し、処理が終わればドメインごと消し去る(=DLLのファイルロックを解放する)ためのエンジンを実装する。

‘Namespace: Enterprise.Core.Engine
Imports System
Imports System.IO
Imports System.Reflection
Imports Enterprise.Plugin.Contract

Public Class PluginHostManager
Implements IDisposable

Private _ad As AppDomain
Private disposedValue As Boolean

”’

”’ 外部DLLを別AppDomainにロードし、IEnterprisePluginインスタンスを取得する
”’

Public Function LoadAndCreatePlugin(ByVal dllPath As String, ByVal typeName As String) As IEnterprisePlugin
‘ セキュリティと分離境界を担保するドメインセットアップ
Dim ads As New AppDomainSetup()
ads.ApplicationBase = Path.GetDirectoryName(dllPath)

‘ 一意のドメイン名を生成
Dim domainName As String = “PluginDomain_” & Guid.NewGuid().ToString(“N”)

‘ 新しいAppDomainを作成
_ad = AppDomain.CreateDomain(domainName, Nothing, ads)

Try
‘ 別AppDomain上でオブジェクトを生成するためのプロキシを作成
Dim targetAssemblyFile As String = Path.GetFullPath(dllPath)

Dim handle As ObjectHandle = _ad.CreateInstanceFrom(
targetAssemblyFile,
typeName
)

Dim unwrap As Object = handle.Unwrap()

If TypeOf unwrap Is IEnterprisePlugin Then
Return DirectCast(unwrap, IEnterprisePlugin)
Else
Throw 0 New InvalidCastException(“指定された型は IEnterprisePlugin を実装していません。”)
End If

Catch ex As Exception
‘ 失敗時は即座にドメインを破棄
UnloadDomain()
Throw New ApplicationException($”プラグインのロードに失敗しました: {ex.Message}”, ex)
End Try
End Function

”’

”’ AppDomainをアンロードし、ファイルロックを完全に解除する
”’

Public Sub UnloadDomain()
If _ad IsNot Nothing Then
Try
AppDomain.Unload(_ad)
Catch ex As Exception
System.Diagnostics.Debug.WriteLine($”AppDomain卸し失敗: {ex.Message}”)
Finally
_ad = Nothing
End Try
End If
End Sub

Region ” IDisposable Support ”
Protected Overridable Sub Dispose(disposing As Boolean)
If Not disposedValue Then
If disposing Then
‘ マネージリソースの解放

タイトルとURLをコピーしました