.Net Core で、特定のインターフェイス実装を探して、実行するというプログラムを書きたいと思って調査した。実際のユースケースは、プロバイダを拡張可能にしたくて、Azure Functions の DIを一か所ではなく、プロバイダごとに書くようにしたかったのだが、Azure Functions ではどうやら、Startupクラスは1つまでしか認識しないっぽいので、自分で委譲するプログラムを書くことにした。
インターフェイス実装クラス
別の namespace にインターフェイス実装したクラスを作成する。インターフェイスは、パラメータありのパターンと無しのパターンを用意した。どちらも同一のプロジェクトにいるので、同じアセンブリになる。
ITalkable interface and Hello class
namespace FindInterfaceImplementation
{
public interface ITalkable
{
void Talk();
void Append(List<string> str);
}
public class Hello : ITalkable
{
public void Talk()
{
Console.WriteLine("Hello!");
}
public void Append(List<string> str)
{
str.Add("Hello!");
}
}
}
world class
namespace FindInterfaceImplementation.World
{
public class World :ITalkable
{
public void Talk()
{
Console.WriteLine("World!");
}
public void Append(List<string> str)
{
str.Add("World!");
}
}
}
インターフェイスを実装したクラスの探索
上記のインターフェイスを実装したクラスを探索する。
Assembly の探索
現在のコードが書かれたデフォルトのアセンブリでよければMicrosoft.Extensions.DependencyModel
をnugetで取得して、DependencyContext.Default.RuntimeLibraries
の名前が一致するものを調べる。参考にしたコードでは、そのDependencyがアセンブリ名で始まるものも対象としている。(なぜそうしているのか?はまだ理解できていません。)
public static IEnumerable<Assembly> GetReferencingAssemblies1(string assemblyName)
{
return DependencyContext.Default.RuntimeLibraries.Where(p => IsCandidateLibrary(p, assemblyName)).Select(
p => Assembly.Load(new AssemblyName(p.Name)));
}
private static bool IsCandidateLibrary(RuntimeLibrary library, string assemblyName)
{
return library.Name == assemblyName || library.Dependencies.Any(d => d.Name.StartsWith(assemblyName));
}
このコードを使うと、assemblyName で指定したアセンブリが取得できるようになります。AssemblyのGetExportedType
メソッドで、public のクラスの型が取れますので、それが、ターゲットのインターフェイスを実装しているかを調査したら完了。多分Linqで書いたらもっとすっきりすると思います。
var name = typeof(Program).Assembly.GetName().Name;
var assemblies = GetReferencingAssemblies1(name);
List<string> result = new List<string>();
foreach (var assembly in assemblies)
{
foreach(var impl in assembly.GetExportedTypes().Where(p => p.GetInterfaces().Contains(typeof(ITalkable))))
{
// 対象のメソッド呼び出し
}
}
クラスの動的な実行
あとは、リフレクションで取得した対象のクラスのインスタンスを生成して実行すれば終了。先ほどのループの中身のみ抜き出します。Activator.CreateInstance
でインスタンスを生成したのち、method.Invokeで単純にメソッドを実行します。リフレクションであっても、参照渡しになるので、List であるresultに値が入って表示されます。
var method = impl.GetMethod("Talk");
var obj = Activator.CreateInstance(impl);
method.Invoke(obj, null);
var method2 = impl.GetMethod("Append");
method2.Invoke(obj, new object[] {result});
実行結果
Hello!
World!
resultの中身を見てみると、しっかり値が入っています。
Console.WriteLine($"Result: {result[0]} {result[1]}");
Result: Hello! World!
まとめ
.NetFramework と .NetCore でやり方が違う様子ですが、これで無事取得でしました。DependencyContext.Load
などのメソッドを使うと、現在のライブラリ以外も検索できるので、拡張性を持ったライブラリなどを作るときには使えそうですね。