Partilhar via


Criar uma aplicação .NET Core com plug-ins

Este tutorial mostra-lhe como criar um plug-in personalizado AssemblyLoadContext para carregar. Um AssemblyDependencyResolver é utilizado para resolver as dependências do plug-in. O tutorial isola corretamente as dependências do plug-in da aplicação de alojamento. Vai aprender a:

Pré-requisitos

  • Instale o SDK .NET 5 ou uma versão mais recente.

Nota

O código de exemplo destina-se a .NET 5, mas todas as funcionalidades que utiliza foram introduzidas no .NET Core 3.0 e estão disponíveis em todas as versões do .NET desde então.

Criar a aplicação

O primeiro passo é criar a aplicação:

  1. Crie uma nova pasta e, nessa pasta, execute o seguinte comando:

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

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

    dotnet sln add AppWithPlugin/AppWithPlugin.csproj
    

Agora podemos preencher o esqueleto da nossa aplicação. Substitua o código no ficheiro 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} --");

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

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

Criar as interfaces de plug-in

O próximo passo na criação de uma aplicação com plug-ins é definir a interface que os plug-ins precisam de implementar. Sugerimos que crie uma biblioteca de classes que contenha quaisquer tipos que planeie utilizar para comunicar entre a sua aplicação e plug-ins. Esta divisão permite-lhe publicar a interface de plug-in como um pacote sem ter de enviar a sua aplicação completa.

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 ficheiro de solução. Elimine o PluginBase/Class1.cs ficheiro e crie um novo ficheiro na pasta com o PluginBase nome ICommand.cs com a seguinte definição de interface:

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

        int Execute();
    }
}

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

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

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

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 fragmento 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 fragmento seguinte:

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

command.Execute();

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

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

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

    foreach (Type type in assembly.GetTypes())
    {
        if (typeof(ICommand).IsAssignableFrom(type))
        {
            ICommand result = Activator.CreateInstance(type) as ICommand;
            if (result != null)
            {
                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}");
    }
}

Plug-ins de carga

Agora, a aplicação pode carregar e instanciar corretamente comandos de assemblagens de plug-in carregadas, mas continua a não conseguir carregar as assemblagens de plug-in. Crie um ficheiro com o nome PluginLoadContext.cs na pasta AppWithPlugin com os seguintes conteúdos:

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 programadores isolar assemblagens carregadas em diferentes grupos para garantir que as versões de assemblagem não entram em conflito. Além disso, um personalizado AssemblyLoadContext pode escolher caminhos diferentes para carregar assemblagens e substituir o comportamento predefinido. Utiliza PluginLoadContext uma instância do AssemblyDependencyResolver tipo introduzida no .NET Core 3.0 para resolver nomes de assemblagem para caminhos. O AssemblyDependencyResolver objeto é construído com o caminho para uma biblioteca de classes .NET. Resolve assemblagens e bibliotecas nativas para os respetivos caminhos relativos com base no ficheiro .deps.json da biblioteca de classes cujo caminho foi passado para o AssemblyDependencyResolver construtor. O personalizado AssemblyLoadContext permite que os plug-ins tenham as suas próprias dependências e facilita o AssemblyDependencyResolver 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(
        Path.GetDirectoryName(
            Path.GetDirectoryName(
                Path.GetDirectoryName(
                    Path.GetDirectoryName(
                        Path.GetDirectoryName(typeof(Program).Assembly.Location)))))));

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

Ao utilizar uma instância diferente PluginLoadContext para cada plug-in, os plug-ins podem ter dependências diferentes ou mesmo em conflito sem problemas.

Plug-in simples sem dependências

Novamente na pasta raiz, faça o seguinte:

  1. Execute o seguinte comando para criar um novo projeto de biblioteca de classes com o nome 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 ficheiro HelloPlugin/Class1.cs por um ficheiro com o nome HelloCommand.cs pelos seguintes conteúdos:

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 ficheiro HelloPlugin.csproj . Deve ter um aspeto semelhante ao seguinte:

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

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

</Project>

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

  <EnableDynamicLoading>true</EnableDynamicLoading>

O <EnableDynamicLoading>true</EnableDynamicLoading> prepara o projeto para que possa ser utilizado como um plug-in. Entre outras coisas, esta ação irá copiar todas as dependências para a saída do projeto. Para obter mais detalhes, veja EnableDynamicLoading.

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

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

O <Private>false</Private> elemento é importante. Isto indica ao MSBuild para não copiar PluginBase.dll para o diretório de saída da HelloPlugin. Se a assemblagem PluginBase.dll estiver presente no diretório de saída, PluginLoadContext irá encontrar a assemblagem e carregá-la quando carregar a assemblagem HelloPlugin.dll . Neste momento, o HelloPlugin.HelloCommand tipo irá implementar a ICommand interface a partir do PluginBase.dll no diretório de saída do HelloPlugin projeto e não na ICommand interface que é carregada para o contexto de carga predefinido. Uma vez que o runtime vê estes dois tipos como tipos diferentes de assemblagens 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 à assemblagem que contém as interfaces de plug-in.

Da mesma forma, o <ExcludeAssets>runtime</ExcludeAssets> elemento também é importante se fizer referência a PluginBase outros pacotes. Esta definição tem o mesmo efeito <Private>false</Private> que, mas funciona em referências de pacotes que o PluginBase projeto ou uma das respetivas dependências pode incluir.

Agora que o HelloPlugin projeto está concluído, 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\net5.0\HelloPlugin.dll" (este caminho pode ser diferente com base na versão do .NET Core que utiliza) 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 "Hello World" e muitos plug-ins têm dependências noutras bibliotecas. Os JsonPlugin projetos e OldJsonPlugin no exemplo mostram dois exemplos de plug-ins com dependências de pacotes NuGet em Newtonsoft.Json. Por este motivo, todos os projetos de plug-in devem adicionar <EnableDynamicLoading>true</EnableDynamicLoading> às propriedades do projeto para que copiem todas as respetivas dependências para o resultado de dotnet build. A publicação da biblioteca de classes com dotnet publish também copiará todas as respetivas 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 AssemblyDependencyResolver objeto também pode resolver bibliotecas nativas, bem como assemblagens de satélite localizadas incluídas em pacotes NuGet. Os UVPlugin e FrenchPlugin no repositório de exemplos demonstram estes cenários.

Referenciar uma interface de plug-in a partir de um pacote NuGet

Digamos que existe uma aplicação A que tem uma interface de plug-in definida no pacote NuGet com o nome A.PluginBase. Como referencia o pacote corretamente no projeto de plug-in? Para referências de projeto, a utilização dos <Private>false</Private> metadados no ProjectReference elemento no ficheiro de projeto impediu que o dll fosse copiado para o resultado.

Para referenciar corretamente o A.PluginBase pacote, quer alterar o <PackageReference> elemento no ficheiro do projeto para o seguinte:

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

Isto impede que as A.PluginBase assemblagens sejam copiadas para o diretório de saída do plug-in e garante que o plug-in utilizará a versão A do A.PluginBase.

Recomendações de arquitetura de destino de plug-in

Uma vez que o carregamento de dependências de plug-in utiliza o ficheiro .deps.json , existe um gotcha relacionado com a arquitetura de destino do plug-in. Especificamente, os plug-ins devem visar um runtime, como .NET 5, em vez de uma versão do .NET Standard. O ficheiro .deps.json é gerado com base na arquitetura que o projeto visa e, uma vez que muitos pacotes compatíveis com o .NET Standard enviam assemblagens de referência para compilação em conjuntos de implementação e .NET Standard para runtimes específicos, o .deps.json pode não ver corretamente assemblagens de implementação ou pode obter a versão .NET Standard de uma assemblagem em vez da versão do .NET Core esperada.

Referências de arquitetura de plug-in

Atualmente, os plug-ins não podem introduzir novas arquiteturas no processo. Por exemplo, não pode carregar um plug-in que utiliza a arquitetura numa aplicação Microsoft.AspNetCore.App que utiliza apenas a arquitetura de raiz Microsoft.NETCore.App . A aplicação anfitriã tem de declarar referências a todas as arquiteturas necessárias pelos plug-ins.