プラグイン間でインターフェイスを共有するC#プラグインアーキテクチャ
-
06-07-2019 - |
質問
時間のない人のために、問題を短いバージョンと長いバージョンに分けました。
ショートバージョン:
プロバイダーおよびコンシューマプラグインを備えたシステムのアーキテクチャが必要です。 プロバイダーはインターフェイスIProviderを実装し、コンシューマーはIConsumerを実装する必要があります。 実行中のアプリケーションは、IProviderとIConsumerのみを認識している必要があります。 コンシューマー実装は、(ServiceProcessorを使用して)どのプロバイダーがInterfaceXを実装し、リストを取得するかを実行中のアセンブリに問い合わせることができます。 これらのIProviderオブジェクトは、InterfaceXが定義するいくつかのイベントにコンシューマをフックできるように、(コンシューマ内の)InterfaceXにキャストする必要があります。実行中のアセンブリが何らかの方法でこのInterfaceXタイプを認識しないため、これは失敗します(キャストは失敗します)。解決策は、プラグインと実行中のアセンブリの両方が参照するアセンブリにInterfaceXを含めることですが、これはすべての新しいプロバイダー/コンシューマーペアの再コンパイルを意味し、非常に望ましくありません。
提案はありますか
ロングバージョン:
プラグインを使用してより高いレベルの再利用性を実現する、ある種の汎用サービスを開発しています。このサービスは、プロバイダーとコンシューマーを使用したある種のオブザーバーパターン実装で構成されています。プロバイダーとコンシューマーの両方がメインアプリケーションのプラグインである必要があります。まず、ソリューションに含まれるプロジェクトをリストして、サービスの仕組みを説明します。
プロジェクトA:すべてのプラグインと基本機能をホストするためのWindowsサービスプロジェクト。 TestGUI Windows Formsプロジェクトは、デバッグを容易にするために使用されます。プロジェクトBのServiceProcessorクラスのインスタンスは、プラグイン関連のものを実行しています。サブフォルダー" Consumers"および「プロバイダ」このプロジェクトのサブフォルダーには、すべてのサブフォルダーがコンシューマまたはプロバイダープラグインをそれぞれ保持するサブフォルダーが含まれています。
プロジェクトB:ServiceProcessorクラス(すべてのプラグインのロードとプラグイン間のディスパッチなど)を保持するクラスライブラリ、IConsumerおよびIProvider。
プロジェクトC:TestConsumer(IConsumerを実装)およびTestProvider(IProviderを実装)で構成されるプロジェクトBにリンクされたクラスライブラリ。追加のインターフェイス(ITest、IProviderから派生したそれ自体)は、TestProviderによって実装されます。
ここでの目標は、ConsumerプラグインがServiceProcessorにどのプロバイダー(少なくともIProviderを実装している)があるかを尋ねることです。返されたIProviderオブジェクトは、コンシューマがITestイベントにイベントハンドラをフックできるように、IConsumer実装で実装する他のインターフェイス(ITest)にキャストする必要があります。
プロジェクトAが開始されると、コンシューマプラグインとプロバイダープラグインを含むサブフォルダーがロードされます。以下は、これまでに遭遇して解決しようとした問題の一部です。
プロジェクトCに常駐していたITestインターフェイスは、TestProviderとTestConsumerが認識しているメソッドとイベントにのみ適用されるためです。一般的な考え方は、プロジェクトAをシンプルにし、プラグインが互いに何をするのかを知らないようにすることです。
プロジェクトCのITestと、IProviderをITestにキャストするTestConsumerのInitializeメソッドのコード(このテストは、ITestを実装するオブジェクトがIConsumerオブジェクトとして知られている場合、単一のクラスライブラリ自体で失敗しません)エラーが発生します。このエラーは、プロジェクトAによっても参照されるプロジェクトBにITestインターフェイスを配置することで解決できます。ただし、新しいインターフェイスをビルドするときにプロジェクトAを再コンパイルする必要があるため、非常に望ましくありません。
プロジェクトCのみが参照する単一のクラスライブラリにITestを配置しようとしましたが、これはプロバイダーとコンシューマーのみがこのインターフェイスを認識する必要があるためです。見つけられた。これは、現在のAppDomainのAssemblyResolveイベントをフックすることで解決できますが、どういうわけかこれも望ましくないようです。 ITestはProjeに戻りました
解決
できる限り最善の方法でソリューションを再現しようとしましたが、そのような問題はありません。 (警告、多くのコードサンプルが続きます。...)
最初のプロジェクトはアプリケーションです。これには1つのクラスが含まれます:
public class PluginLoader : ILoader
{
private List<Type> _providers = new List<Type>();
public PluginLoader()
{
LoadProviders();
LoadConsumers();
}
public IProvider RequestProvider(Type providerType)
{
foreach(Type t in _providers)
{
if (t.GetInterfaces().Contains(providerType))
{
return (IProvider)Activator.CreateInstance(t);
}
}
return null;
}
private void LoadProviders()
{
DirectoryInfo di = new DirectoryInfo(PluginSearchPath);
FileInfo[] assemblies = di.GetFiles("*.dll");
foreach (FileInfo assembly in assemblies)
{
Assembly a = Assembly.LoadFrom(assembly.FullName);
foreach (Type type in a.GetTypes())
{
if (type.GetInterfaces().Contains(typeof(IProvider)))
{
_providers.Add(type);
}
}
}
}
private void LoadConsumers()
{
DirectoryInfo di = new DirectoryInfo(PluginSearchPath);
FileInfo[] assemblies = di.GetFiles("*.dll");
foreach (FileInfo assembly in assemblies)
{
Assembly a = Assembly.LoadFrom(assembly.FullName);
foreach (Type type in a.GetTypes())
{
if (type.GetInterfaces().Contains(typeof(IConsumer)))
{
IConsumer consumer = (IConsumer)Activator.CreateInstance(type);
consumer.Initialize(this);
}
}
}
}
明らかにこれは非常にきれいにできます。
次のプロジェクトは、次の3つのインターフェイスを含む共有ライブラリです。
public interface ILoader
{
IProvider RequestProvider(Type providerType);
}
public interface IConsumer
{
void Initialize(ILoader loader);
}
public interface IProvider
{
}
最後に、これらのクラスを含むプラグインプロジェクトがあります:
public interface ITest : IProvider
{
}
public class TestConsumer : IConsumer
{
public void Initialize(ILoader loader)
{
ITest tester = (ITest)loader.RequestProvider(typeof (ITest));
}
}
public class TestProvider : ITest
{
}
アプリケーションとプラグインプロジェクトの両方が共有プロジェクトを参照し、プラグインdllがアプリケーションの検索ディレクトリにコピーされますが、相互に参照しません。
PluginLoaderが構築されると、すべてのIProviderが検出され、すべてのIConsumersが作成され、それらに対してInitializeが呼び出されます。初期化内で、コンシューマーはローダーからプロバイダーを要求できます。このコードの場合、TestProviderが構築されて返されます。これらはすべて、アセンブリの読み込みを細かく制御することなく機能します。
他のヒント
まだ開発中ですが、MEF(.Net 4に含まれる)の完全なユースケースのように聞こえ、VS2010で内部的に使用されます。
MEFは、 ランタイムの拡張性の問題。まで 今、したい任意のアプリケーション に必要なプラグインモデルをサポートする 独自のインフラストラクチャを作成する スクラッチ。これらのプラグインはしばしば アプリケーション固有であり、できませんでした 複数で再利用 実装。
プレビューは http://www.codeplex.com/MEF
で既に利用可能です。 >グレンブロックのブログも役立ちます。
プラグインフレームワークの実例と、インターフェイスを保持する共通のアセンブリを作成することでこれらの問題に対処する方法を確認するには、私の記事が役立つかもしれません。
C#基本チュートリアルのプラグイン:
http://www.codeproject.com/KB/cs/pluginsincsharp.aspx
フォローアップ記事、Genericsを有効にしたプラグインマネージャーライブラリ:
http://www.codeproject.com/KB/cs/ExtensionManagerLibrary.aspx
2つの無関係なアセンブリが同じインターフェイスを共有する方法は疑問です。答えは「できない」です。解決策は、すべてのアセンブリにインターフェイスを含めることです。おそらく、プラグインビルダーが参照できるdllに、ロードアセンブリ内。
あなたがやろうとしていることと似たようなことをしましたが、ローダーが自動的に見える場所にアセンブリがある限り、問題は発生しませんでした。
すべてのアセンブリを、exeが存在する下のサブディレクトリに配置しようとしましたか?詳細は今思い出せませんが、ローダーがアセンブリ/タイプを探す正確な場所と順序について文書化されたステップのリストがあります。