前回は、リフレクションで型の中身を調べる方法を見ました。
今回は一歩進んで、外部アセンブリを実行時に読み込み、型を探し、インスタンスを作り、メソッドを呼び出します。
この考え方は、プラグイン機構や拡張機能を作るときの基本になります。
実行時に読み込むとはどういうことか
通常の C# アプリケーションでは、参照するライブラリはビルド時に決まっています。
<ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
一方、拡張可能なアプリケーションでは、あとから追加された DLL を実行時に読み込みたいことがあります。
たとえば、次のような構成です。
App/
ExtensibleApp.exe
Plugins/
CsvImporter.dll
JsonImporter.dll
アプリ本体は、起動時に Plugins フォルダーを調べ、そこにある DLL を読み込みます。
これにより、アプリ本体を再ビルドしなくても機能を追加できます。
外部アセンブリを読み込む
もっとも単純には、Assembly.LoadFrom() で DLL を読み込めます。
using System.Reflection;
string pluginPath = @"C:\Plugins\SamplePlugin.dll";
Assembly assembly = Assembly.LoadFrom(pluginPath);
Console.WriteLine(assembly.FullName);
現代の .NET でプラグインを本格的に扱うなら、依存関係やアンロードを考えて AssemblyLoadContext を使う方が柔軟です。
ただし、まず仕組みを理解する入口としては Assembly.LoadFrom() でも十分です。
型を探す
読み込んだアセンブリから型を列挙できます。
using System.Reflection;
Assembly assembly = Assembly.LoadFrom(@"C:\Plugins\SamplePlugin.dll");
foreach (Type type in assembly.GetTypes())
{
Console.WriteLine(type.FullName);
}
特定の名前の型を探すなら、次のようにします。
Type? pluginType = assembly.GetTypes()
.FirstOrDefault(type => type.Name == "SamplePlugin");
if (pluginType == null)
{
Console.WriteLine("型が見つかりません。");
return;
}
名前だけで探す方法は簡単ですが、壊れやすいです。
実務では、後で見るように共通インターフェイスや属性で探す方が安全です。
インスタンスを作成する
Activator.CreateInstance() を使うと、実行時に型からインスタンスを作れます。
object? instance = Activator.CreateInstance(pluginType);
if (instance == null)
{
Console.WriteLine("インスタンスを作成できませんでした。");
return;
}
引数ありのコンストラクターも呼び出せます。
object? instance = Activator.CreateInstance(
pluginType,
"Plugin Name",
3);
ただし、引数の順番や型を間違えると実行時例外になります。
静的型付けの安全性は弱くなるため、境界を小さく保つことが大切です。
引数なしメソッドを呼び出す
MethodInfo.Invoke() を使うと、実行時にメソッドを呼び出せます。
using System.Reflection;
MethodInfo? method = pluginType.GetMethod("Run");
if (method == null)
{
Console.WriteLine("Run メソッドが見つかりません。");
return;
}
method.Invoke(instance, parameters: null);
このコードは、コンパイル時には Run メソッドの存在を知りません。
実行時にメタデータを調べて呼び出しています。
引数ありメソッドを呼び出す
引数を渡す場合は、object?[] を使います。
MethodInfo? method = pluginType.GetMethod("Execute");
if (method == null)
{
Console.WriteLine("Execute メソッドが見つかりません。");
return;
}
object? result = method.Invoke(
instance,
new object?[] { "input.txt", 10 });
Console.WriteLine(result);
戻り値も object? として返ります。
必要に応じてキャストします。
if (result is string message)
{
Console.WriteLine(message);
}
共通インターフェイスで安全に呼び出す
遅延バインディングは柔軟ですが、文字列ベースの呼び出しは壊れやすいです。
拡張可能なアプリでは、共通インターフェイスを用意する設計が扱いやすくなります。
namespace PluginContracts;
public interface IPlugin
{
string Name { get; }
void Execute();
}
プラグイン側です。
using PluginContracts;
public class HelloPlugin : IPlugin
{
public string Name => "Hello";
public void Execute()
{
Console.WriteLine("Hello from plugin.");
}
}
アプリ本体側では、読み込んだ型から IPlugin を実装しているものを探します。
using System.Reflection;
using PluginContracts;
Assembly assembly = Assembly.LoadFrom(@"C:\Plugins\HelloPlugin.dll");
Type? pluginType = assembly.GetTypes()
.FirstOrDefault(type =>
typeof(IPlugin).IsAssignableFrom(type) &&
type is { IsClass: true, IsAbstract: false });
if (pluginType == null)
{
Console.WriteLine("プラグイン型が見つかりません。");
return;
}
IPlugin plugin = (IPlugin)Activator.CreateInstance(pluginType)!;
Console.WriteLine(plugin.Name);
plugin.Execute();
この形にすると、実行時に型を探しつつ、呼び出し自体はインターフェイス経由で型安全にできます。
フレームワークのアセンブリを調べる
リフレクションは、自分のアセンブリだけでなく、フレームワークの型にも使えます。
using System.Reflection;
Type type = typeof(HttpClient);
Assembly assembly = type.Assembly;
Console.WriteLine(assembly.FullName);
foreach (MethodInfo method in type.GetMethods()
.Where(m => m.Name.Contains("Async")))
{
Console.WriteLine(method.Name);
}
ドキュメント生成、API 調査、学習用ツールなどで役立ちます。
動的読み込み時の注意点
外部 DLL を読み込む設計では、次の点に注意します。
- 読み込む DLL の信頼性
- 依存アセンブリの解決
- バージョン違い
- 例外の隔離
- 実行権限
- アンロードが必要かどうか
特に、ユーザーが自由に置ける DLL を読み込む場合は、セキュリティ上の境界として扱うべきではありません。
同じプロセス内で実行する以上、プラグインはアプリ本体へ大きな影響を与えられます。
信頼できないコードを実行する必要があるなら、別プロセス化やコンテナー化など、より強い分離を検討します。
まとめ
外部アセンブリを読み込み、型を探し、インスタンスを作り、メソッドを呼び出すことで、実行時に機能を追加できます。
ただし、文字列でメソッド名を指定する設計は壊れやすいため、実務では共通インターフェイスや属性を組み合わせるのがおすすめです。
次回は、その属性を使って、型やメソッドに意味を付与する方法を見ていきます。