次の方法で共有


プラグインを使用して .NET Core アプリケーションを作成する

このチュートリアルでは、プラグインを読み込むカスタム AssemblyLoadContext を作成する方法について説明します。 AssemblyDependencyResolverは、プラグインの依存関係を解決するために使用されます。 このチュートリアルでは、プラグインの依存関係に対して個別のアセンブリ コンテキストを提供します。これにより、プラグインとホスティング アプリケーション間で異なるアセンブリ依存関係が可能になります。 次の方法について学習します。

  • プラグインをサポートするプロジェクトを構築します。
  • 各プラグインを読み込むためのカスタムAssemblyLoadContextを作成します。
  • プラグインに依存関係を持たせるようにするには、 System.Runtime.Loader.AssemblyDependencyResolver 型を使用します。
  • ビルド成果物をコピーするだけで簡単にデプロイできるプラグインを作成します。

信頼されていないコードを信頼された .NET プロセスに安全に読み込むことはできません。 セキュリティまたは信頼性の境界を提供するには、OS または仮想化プラットフォームによって提供されるテクノロジを検討してください。

[前提条件]

アプリケーションを作成する

最初の手順では、アプリケーションを作成します。

  1. 新しいフォルダーを作成し、そのフォルダーで次のコマンドを実行します。

    dotnet new console -o AppWithPlugin
    
  2. プロジェクトのビルドを容易にするには、同じフォルダーに Visual Studio ソリューション ファイルを作成します。 次のコマンドを実行します。

    dotnet new sln
    
  3. 次のコマンドを実行して、アプリ プロジェクトをソリューションに追加します。

    dotnet sln add AppWithPlugin/AppWithPlugin.csproj
    

これで、アプリケーションの基本構造を埋めることができます。 AppWithPlugin/Program.cs ファイル内のコードを次のコードに置き換えます。

using PluginBase;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

namespace AppWithPlugin
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                if (args.Length == 1 && args[0] == "/d")
                {
                    Console.WriteLine("Waiting for any key...");
                    Console.ReadLine();
                }

                // Load commands from plugins.

                if (args.Length == 0)
                {
                    Console.WriteLine("Commands: ");
                    // Output the loaded commands.
                }
                else
                {
                    foreach (string commandName in args)
                    {
                        Console.WriteLine($"-- {commandName} --");

                        // Skip command-line switches/flags (arguments starting with '/' or '-')
                        if (commandName.StartsWith("/") || commandName.StartsWith("-"))
                        {
                            Console.WriteLine($"Skipping command-line flag: {commandName}");
                            continue;
                        }

                        // Execute the command with the name passed as an argument.

                        Console.WriteLine();
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }
    }
}

プラグイン インターフェイスを作成する

プラグインを使用してアプリを構築する次の手順は、プラグインが実装する必要があるインターフェイスを定義することです。 アプリとプラグイン間の通信に使用する予定の型を含むクラス ライブラリを作成することをお勧めします。 この部門では、完全なアプリケーションを出荷することなく、プラグインインターフェイスをパッケージとして公開することができます。

プロジェクトのルート フォルダーで、 dotnet new classlib -o PluginBaseを実行します。 また、 dotnet sln add PluginBase/PluginBase.csproj を実行して、ソリューション ファイルにプロジェクトを追加します。 PluginBase/Class1.cs ファイルを削除し、次のインターフェイス定義を持つ PluginBase という名前のICommand.cs フォルダーに新しいファイルを作成します。

namespace PluginBase
{
    public interface ICommand
    {
        string Name { get; }
        string Description { get; }

        int Execute();
    }
}

この ICommand インターフェイスは、すべてのプラグインが実装するインターフェイスです。

ICommand インターフェイスが定義されたので、アプリケーション プロジェクトをもう少し入力できます。 ルート フォルダーの AppWithPlugin コマンドを使用して、PluginBase プロジェクトから dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj プロジェクトへの参照を追加します。

// Load commands from pluginsコメントを次のコード スニペットに置き換えて、指定されたファイル パスからプラグインを読み込めるようにします。

string[] pluginPaths = new string[]
{
    // Paths to plugins to load.
};

IEnumerable<ICommand> commands = pluginPaths.SelectMany(pluginPath =>
{
    Assembly pluginAssembly = LoadPlugin(pluginPath);
    return CreateCommands(pluginAssembly);
}).ToList();

次に、 // Output the loaded commands コメントを次のコード スニペットに置き換えます。

foreach (ICommand command in commands)
{
    Console.WriteLine($"{command.Name}\t - {command.Description}");
}

// Execute the command with the name passed as an argumentコメントを次のスニペットに置き換えます。

ICommand command = commands.FirstOrDefault(c => c.Name == commandName);
if (command == null)
{
    Console.WriteLine($"No such command is known: {commandName}");
    continue;
}

command.Execute();

最後に、次に示すように、Program および LoadPlugin という名前のCreateCommands クラスに静的メソッドを追加します。

static Assembly LoadPlugin(string relativePath)
{
    throw new NotImplementedException();
}

static IEnumerable<ICommand> CreateCommands(Assembly assembly)
{
    int count = 0;

    foreach (var type in assembly.GetTypes().Where(t => typeof(ICommand).IsAssignableFrom(t)))
    {
        if (Activator.CreateInstance(type) is ICommand result)
        {
            count++;
            yield return result;
        }
    }

    if (count == 0)
    {
        string availableTypes = string.Join(",", assembly.GetTypes().Select(t => t.FullName));
        throw new ApplicationException(
            $"Can't find any type which implements ICommand in {assembly} from {assembly.Location}.\n" +
            $"Available types: {availableTypes}");
    }
}

プラグインを読み込む

これで、アプリケーションは読み込まれたプラグイン アセンブリからコマンドを正しく読み込んでインスタンス化できますが、プラグイン アセンブリを読み込むことができません。 AppWithPlugin フォルダーに PluginLoadContext.cs という名前のファイルを作成し、次の内容を指定します。

using System;
using System.Reflection;
using System.Runtime.Loader;

namespace AppWithPlugin
{
    class PluginLoadContext : AssemblyLoadContext
    {
        private AssemblyDependencyResolver _resolver;

        public PluginLoadContext(string pluginPath)
        {
            _resolver = new AssemblyDependencyResolver(pluginPath);
        }

        protected override Assembly Load(AssemblyName assemblyName)
        {
            string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
            if (assemblyPath != null)
            {
                return LoadFromAssemblyPath(assemblyPath);
            }

            return null;
        }

        protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
        {
            string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
            if (libraryPath != null)
            {
                return LoadUnmanagedDllFromPath(libraryPath);
            }

            return IntPtr.Zero;
        }
    }
}

PluginLoadContext型はAssemblyLoadContextから派生します。 AssemblyLoadContext型はランタイムの特殊な型です。これにより、開発者は読み込まれたアセンブリを別のグループに分離して、アセンブリのバージョンが競合しないようにすることができます。 さらに、カスタム AssemblyLoadContext は、アセンブリを読み込むさまざまなパスを選択し、既定の動作をオーバーライドできます。 PluginLoadContextでは、.NET Core 3.0 で導入されたAssemblyDependencyResolver型のインスタンスを使用して、アセンブリ名をパスに解決します。 AssemblyDependencyResolver オブジェクトは、.NET クラス ライブラリへのパスを使用して構築されます。 アセンブリとネイティブ ライブラリは、パスが コンストラクターに渡されたクラス ライブラリの AssemblyDependencyResolver ファイルに基づいて、それらの相対パスに解決されます。 カスタム AssemblyLoadContext を使用すると、プラグインは独自の依存関係を持つことができます。 AssemblyDependencyResolver により、依存関係を簡単に正しく読み込むことができます。

AppWithPlugin プロジェクトにPluginLoadContext型が設定されたので、次の本文で Program.LoadPlugin メソッドを更新します。

static Assembly LoadPlugin(string relativePath)
{
    // Navigate up to the solution root
    string root = Path.GetFullPath(
        Path.Combine(typeof(Program).Assembly.Location, "..", "..", "..", "..", ".."));

    string pluginLocation = Path.GetFullPath(Path.Combine(root, relativePath.Replace('\\', Path.DirectorySeparatorChar)));
    Console.WriteLine($"Loading commands from: {pluginLocation}");
    PluginLoadContext loadContext = new(pluginLocation);
    return loadContext.LoadFromAssemblyName(new(Path.GetFileNameWithoutExtension(pluginLocation)));
}

プラグインごとに異なる PluginLoadContext インスタンスを使用することで、プラグインは問題なく異なる依存関係や競合する依存関係を持つことができます。

依存関係のないシンプルなプラグイン

ルート フォルダーに戻り、次の操作を行います。

  1. 次のコマンドを実行して、 HelloPluginという名前の新しいクラス ライブラリ プロジェクトを作成します。

    dotnet new classlib -o HelloPlugin
    
  2. 次のコマンドを実行して、プロジェクトを AppWithPlugin ソリューションに追加します。

    dotnet sln add HelloPlugin/HelloPlugin.csproj
    
  3. HelloPlugin/Class1.cs ファイルを、HelloCommand.csという名前のファイルに次の内容で置き換えます。

using PluginBase;
using System;

namespace HelloPlugin
{
    public class HelloCommand : ICommand
    {
        public string Name { get => "hello"; }
        public string Description { get => "Displays hello message."; }

        public int Execute()
        {
            Console.WriteLine("Hello !!!");
            return 0;
        }
    }
}

次に、 HelloPlugin.csproj ファイルを 開きます。 次のようになります。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

<PropertyGroup> タグの間に、次の要素を追加します。

  <EnableDynamicLoading>true</EnableDynamicLoading>

<EnableDynamicLoading>true</EnableDynamicLoading>は、プラグインとして使用できるようにプロジェクトを準備します。 特に、すべての依存関係がプロジェクトの出力にコピーされます。 詳細については、EnableDynamicLoadingを参照してください。

<Project> タグの間に、次の要素を追加します。

<ItemGroup>
    <ProjectReference Include="..\PluginBase\PluginBase.csproj">
        <Private>false</Private>
        <ExcludeAssets>runtime</ExcludeAssets>
    </ProjectReference>
</ItemGroup>

<Private>false</Private>要素は重要です。 これにより、helloPlugin の出力ディレクトリに PluginBase.dll をコピーしないように MSBuild に指示されます。 PluginBase.dll アセンブリが出力ディレクトリに存在する場合、PluginLoadContextはアセンブリを検索し、HelloPlugin.dll アセンブリを読み込むと読み込みます。 この時点で、HelloPlugin.HelloCommand型は、既定の読み込みコンテキストに読み込まれるICommandインターフェイスではなく、HelloPluginプロジェクトの出力ディレクトリにあるPluginBase.dllからICommandインターフェイスを実装します。 ランタイムは、これら 2 つの型を異なるアセンブリとは異なる型として見なすので、 AppWithPlugin.Program.CreateCommands メソッドはコマンドを見つけることができません。 その結果、プラグイン インターフェイスを含むアセンブリへの参照には、 <Private>false</Private> メタデータが必要になります。

同様に、 <ExcludeAssets>runtime</ExcludeAssets> 要素は、 PluginBase が他のパッケージを参照する場合にも重要です。 この設定は <Private>false</Private> と同じ効果を持ちますが、 PluginBase プロジェクトまたはその依存関係の 1 つが含まれるパッケージ参照で機能します。

HelloPlugin プロジェクトが完了したら、AppWithPlugin プロジェクトを更新して、HelloPlugin プラグインが見つかる場所を確認する必要があります。 // Paths to plugins to loadコメントの後に、@"HelloPlugin\bin\Debug\net10.0\HelloPlugin.dll"配列の要素としてpluginPaths (このパスは、使用する .NET Core のバージョンによって異なる場合があります) を追加します。

ライブラリの依存関係を持つプラグイン

ほぼすべてのプラグインは単純な "Hello World" よりも複雑であり、多くのプラグインは他のライブラリに依存しています。 サンプルの JsonPlugin プロジェクトと OldJsonPlugin プロジェクトは、NuGet パッケージが Newtonsoft.Jsonに依存するプラグインの 2 つの例を示しています。 このため、すべてのプラグイン プロジェクトは、すべての依存関係を <EnableDynamicLoading>true</EnableDynamicLoading> の出力にコピーするように、プロジェクトのプロパティにdotnet buildを追加する必要があります。 dotnet publishを使用してクラス ライブラリを発行すると、その依存関係もすべて発行出力にコピーされます。

サンプルのその他の例

このチュートリアルの完全なソース コードは 、dotnet/samples リポジトリにあります。 完成したサンプルには、 AssemblyDependencyResolver 動作の他のいくつかの例が含まれています。 たとえば、 AssemblyDependencyResolver オブジェクトは、ネイティブ ライブラリだけでなく、NuGet パッケージに含まれるローカライズされたサテライト アセンブリも解決できます。 サンプル リポジトリの UVPluginFrenchPlugin は、これらのシナリオを示しています。

NuGet パッケージからプラグイン インターフェイスを参照する

たとえば、 A.PluginBase という名前の NuGet パッケージで定義されたプラグイン インターフェイスを持つアプリ A があるとします。 プラグイン プロジェクトでパッケージを正しく参照するにはどうすればよいですか? プロジェクト参照の場合、プロジェクト ファイル内の <Private>false</Private> 要素でProjectReferenceメタデータを使用すると、dll が出力にコピーされなくなります。

A.PluginBase パッケージを正しく参照するには、プロジェクト ファイル内の<PackageReference>要素を次のように変更します。

<PackageReference Include="A.PluginBase" Version="1.0.0">
    <ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>

これにより、 A.PluginBase アセンブリがプラグインの出力ディレクトリにコピーされるのを防ぎ、プラグインで A のバージョンの A.PluginBaseが使用されるようになります。

プラグイン ターゲット フレームワークの推奨事項

プラグインの依存関係の読み込みでは .deps.json ファイルが使用されるため、プラグインのターゲット フレームワークに関連する問題があります。 具体的には、プラグインは、.NET Standard のバージョンではなく、.NET 10 などのランタイムを対象とする必要があります。 .deps.json ファイルは、プロジェクトがターゲットとするフレームワークに基づいて生成されます。多くの .NET Standard 互換パッケージは、.NET Standard および特定のランタイムの実装アセンブリに対してビルドするための参照アセンブリを出荷するため、.deps.json で実装アセンブリが正しく表示されない場合や、予想される .NET Core バージョンではなく.NET Standard バージョンのアセンブリを取得する可能性があります。

プラグイン フレームワークのリファレンス

現時点では、プラグインは新しいフレームワークをプロセスに導入できません。 たとえば、ルート Microsoft.AspNetCore.App フレームワークのみを使用するアプリケーションに、Microsoft.NETCore.App フレームワークを使用するプラグインを読み込むことはありません。 ホスト アプリケーションは、プラグインで必要なすべてのフレームワークへの参照を宣言する必要があります。