Compartilhar via


Criar um aplicativo .NET Core com plug-ins

Este tutorial mostra como criar um componente personalizado AssemblyLoadContext para carregar plug-ins. Um AssemblyDependencyResolver é usado para resolver as dependências do plug-in. O tutorial fornece um contexto de assembly separado para as dependências do plug-in, permitindo diferentes dependências de assembly entre os plug-ins e o aplicativo de hospedagem. Você aprenderá a:

  • Estruturar um projeto para dar suporte a plug-ins.
  • Crie um AssemblyLoadContext personalizado para carregar cada plugin.
  • Use o System.Runtime.Loader.AssemblyDependencyResolver tipo para permitir que os plug-ins tenham dependências.
  • Desenvolva plug-ins que podem ser facilmente implantados apenas copiando os artefatos de compilação.

Observação

Código não confiável não pode ser carregado com segurança em um processo confiável do .NET. Para fornecer um limite de segurança ou confiabilidade, considere uma tecnologia fornecida pelo sistema operacional ou pela plataforma de virtualização.

Pré-requisitos

Criar o aplicativo

A primeira etapa é criar o aplicativo:

  1. Crie uma pasta e nela execute o seguinte comando:

    dotnet new console -o AppWithPlugin
    
  2. Para facilitar a criação do projeto, crie um arquivo de solução do Visual Studio na mesma pasta. Execute o comando a seguir:

    dotnet new sln
    
  3. Execute o seguinte comando para adicionar o projeto de aplicativo à solução:

    dotnet sln add AppWithPlugin/AppWithPlugin.csproj
    

Agora podemos preencher o esqueleto de nossa aplicação. Substitua o código no arquivo AppWithPlugin/Program.cs pelo seguinte código:

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);
            }
        }
    }
}

Criar as interfaces do plug-in

A próxima etapa na criação de um aplicativo com plug-ins é definir a interface que os plug-ins precisam implementar. Sugerimos que você crie uma biblioteca de classes que contenha todos os tipos que você planeja usar para se comunicar entre seu aplicativo e plug-ins. Essa divisão permite que você publique sua interface de plug-in como um pacote sem precisar enviar seu aplicativo completo.

Na pasta raiz do projeto, execute dotnet new classlib -o PluginBase. Além disso, execute dotnet sln add PluginBase/PluginBase.csproj para adicionar o projeto ao arquivo de solução. Exclua o PluginBase/Class1.cs arquivo e crie um novo arquivo na PluginBase pasta nomeada ICommand.cs com a seguinte definição de interface:

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

        int Execute();
    }
}

Essa ICommand interface é a interface que todos os plug-ins implementarão.

Agora que a ICommand interface está definida, o projeto de aplicativo pode ser preenchido um pouco mais. Adicione uma referência do projeto AppWithPlugin para o projeto PluginBase com o comando dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj da pasta raiz.

Substitua o // Load commands from plugins comentário pelo seguinte snippet de código para permitir que ele carregue plug-ins de determinados caminhos de arquivo:

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

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

Em seguida, substitua o // Output the loaded commands comentário pelo seguinte snippet de código:

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

Substitua o // Execute the command with the name passed as an argument comentário pelo seguinte snippet:

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

command.Execute();

Por fim, adicione métodos estáticos à Program classe nomeada LoadPlugin e CreateCommands, conforme mostrado aqui:

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}");
    }
}

Carregar plug-ins

Agora, o aplicativo pode carregar e instanciar corretamente comandos dos assemblies de plug-in que foram carregados, mas ainda é incapaz de carregar os assemblies de plug-in. Crie um arquivo chamado PluginLoadContext.cs na pasta AppWithPlugin com o seguinte conteúdo:

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;
        }
    }
}

O PluginLoadContext tipo deriva de AssemblyLoadContext. O AssemblyLoadContext tipo é um tipo especial no runtime que permite aos desenvolvedores isolar assemblies carregados em grupos diferentes para garantir que as versões do assembly não entrem em conflito. Além disso, um personalizado AssemblyLoadContext pode escolher caminhos diferentes para carregar assemblies e substituir o comportamento padrão. O PluginLoadContext usa uma instância do tipo AssemblyDependencyResolver introduzida no .NET Core 3.0 para resolver nomes de assembly em caminhos. O AssemblyDependencyResolver objeto é construído com o caminho para uma biblioteca de classes do .NET. Ele resolve assemblies e bibliotecas nativas para seus caminhos relativos com base no arquivo .deps.json da biblioteca de classes cujo caminho foi passado para o AssemblyDependencyResolver construtor. A personalização AssemblyLoadContext permite aos plug-ins terem suas próprias dependências, e o AssemblyDependencyResolver facilita o carregamento correto das dependências.

Agora que o AppWithPlugin projeto tem o PluginLoadContext tipo, atualize o Program.LoadPlugin método com o seguinte corpo:

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)));
}

Usando uma instância diferente PluginLoadContext para cada plug-in, os plug-ins podem ter dependências diferentes ou até mesmo conflitantes sem problemas.

Plug-in simples sem dependências

De volta à pasta raiz, faça o seguinte:

  1. Execute o seguinte comando para criar um novo projeto de biblioteca de classes chamado HelloPlugin:

    dotnet new classlib -o HelloPlugin
    
  2. Execute o seguinte comando para adicionar o projeto à AppWithPlugin solução:

    dotnet sln add HelloPlugin/HelloPlugin.csproj
    
  3. Substitua o arquivo HelloPlugin/Class1.cs por um arquivo chamado HelloCommand.cs pelo seguinte conteúdo:

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;
        }
    }
}

Agora, abra o arquivo HelloPlugin.csproj . Ele deve ser semelhante ao seguinte:

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

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

</Project>

Entre as <PropertyGroup> marcas, adicione o seguinte elemento:

  <EnableDynamicLoading>true</EnableDynamicLoading>

O <EnableDynamicLoading>true</EnableDynamicLoading> prepara o projeto para que ele possa ser usado como um plug-in. Entre outras coisas, esse processo copiará todas as suas dependências para a saída do projeto. Para obter mais detalhes, consulte EnableDynamicLoading.

Entre as <Project> marcas, adicione os seguintes elementos:

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

O <Private>false</Private> elemento é importante. Isso instrui o MSBuild a não copiar o arquivo PluginBase.dll para o diretório de saída do HelloPlugin. Se o assemblyPluginBase.dll estiver presente no diretório de saída, PluginLoadContext encontrará o assembly lá e o carregará quando carregar o assemblyHelloPlugin.dll . Neste ponto, o tipo HelloPlugin.HelloCommand implementará a interface ICommand do PluginBase.dll no diretório de saída do projeto HelloPlugin, não a interface ICommand que é carregada no contexto de carga padrão. Como o runtime vê esses dois tipos como tipos diferentes de assemblies diferentes, o AppWithPlugin.Program.CreateCommands método não encontrará os comandos. Como resultado, os <Private>false</Private> metadados são necessários para a referência ao assembly que contém as interfaces do plugin.

Da mesma forma, o <ExcludeAssets>runtime</ExcludeAssets> elemento também será importante se o PluginBase referenciar outros pacotes. Essa configuração tem o mesmo efeito que <Private>false</Private>, mas funciona em referências de pacote que o PluginBase projeto ou uma de suas dependências pode incluir.

Agora que o HelloPlugin projeto está concluído, você deve atualizar o AppWithPlugin projeto para saber onde o HelloPlugin plug-in pode ser encontrado. Após o // Paths to plugins to load comentário, adicione @"HelloPlugin\bin\Debug\net10.0\HelloPlugin.dll" (esse caminho pode ser diferente com base na versão do .NET Core que você usa) como um elemento da pluginPaths matriz.

Plug-in com dependências de biblioteca

Quase todos os plug-ins são mais complexos do que um simples "Olá, Mundo", e muitos plug-ins têm dependências em outras bibliotecas. Os projetos JsonPlugin e OldJsonPlugin no exemplo mostram dois exemplos de plug-ins com dependências de pacote NuGet em Newtonsoft.Json. Por isso, todos os projetos de plug-in devem adicionar <EnableDynamicLoading>true</EnableDynamicLoading> às propriedades do projeto para que copiem todas as dependências para a saída de dotnet build. A dotnet publish publicação da biblioteca de classes também copiará todas as suas dependências para a saída de publicação.

Outros exemplos no exemplo

O código-fonte completo deste tutorial pode ser encontrado no repositório dotnet/samples. O exemplo concluído inclui alguns outros exemplos de AssemblyDependencyResolver comportamento. Por exemplo, o objeto AssemblyDependencyResolver pode também resolver bibliotecas nativas, bem como assemblies satélite localizados que estão incluídos em pacotes NuGet. UVPlugin e FrenchPlugin no repositório de exemplos demonstram esses cenários.

Referenciar uma interface de plug-in de um pacote NuGet

Digamos que haja um aplicativo A que tenha uma interface de plug-in definida no pacote NuGet chamado A.PluginBase. Como você faz referência ao pacote corretamente em seu projeto de plug-in? Para referências de projeto, o uso do metadado <Private>false</Private> no elemento ProjectReference do arquivo de projeto impediu que a dll fosse copiada para o diretório de saída.

Para referenciar corretamente o A.PluginBase pacote, você deseja alterar o <PackageReference> elemento no arquivo de projeto para o seguinte:

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

Isso impede que os A.PluginBase assemblies sejam copiados para o diretório de saída do plug-in e garante que o plug-in use a versão A de A.PluginBase.

Recomendações do framework de destino do plug-in

Como o carregamento de dependência do plug-in usa o arquivo .deps.json , há um gotcha relacionado à estrutura de destino do plug-in. Especificamente, seus plug-ins devem ter como destino um runtime, como o .NET 10, em vez de uma versão do .NET Standard. O arquivo .deps.json é gerado com base em qual estrutura de destino do projeto, e visto que muitos pacotes compatíveis com .NET Standard fornecem assemblies de referência para compilação contra o .NET Standard e assemblies de implementação para runtimes específicos, o .deps.json pode não identificar corretamente os assemblies de implementação ou pode pegar a versão do .NET Standard de um assembly em vez da versão do .NET Core esperada.

Referências da estrutura do plug-in

Atualmente, os plug-ins não podem introduzir novas estruturas no processo. Por exemplo, você não pode carregar um plug-in que usa a Microsoft.AspNetCore.App estrutura em um aplicativo que usa apenas a estrutura raiz Microsoft.NETCore.App . O aplicativo host deve declarar referências a todas as estruturas necessárias pelos plug-ins.