Compartir vía


Creación de una aplicación de .NET Core con complementos

En este tutorial se muestra cómo crear un personalizado AssemblyLoadContext para cargar complementos. AssemblyDependencyResolver se usa para resolver las dependencias del complemento. El tutorial proporciona un contexto de ensamblado independiente para las dependencias del complemento, lo que permite diferentes dependencias de ensamblado entre los complementos y la aplicación de hospedaje. Aprenderá a:

  • Estructurar un proyecto para admitir complementos.
  • Cree un elemento personalizado AssemblyLoadContext para cargar cada complemento.
  • Use el System.Runtime.Loader.AssemblyDependencyResolver tipo para permitir que los complementos tengan dependencias.
  • Crea complementos que se puedan implementar fácilmente al simplemente copiar los artefactos del build.

Nota:

El código que no es de confianza no se puede cargar de forma segura en un proceso de .NET de confianza. Para proporcionar un límite de seguridad o confiabilidad, considere la posibilidad de usar una tecnología proporcionada por el sistema operativo o la plataforma de virtualización.

Prerrequisitos

Creación de la aplicación

El primer paso es crear la aplicación:

  1. Cree una carpeta nueva y, en ella, ejecute el siguiente comando:

    dotnet new console -o AppWithPlugin
    
  2. Para facilitar la compilación del proyecto, cree un archivo de solución de Visual Studio en la misma carpeta. Ejecute el siguiente comando:

    dotnet new sln
    
  3. Ejecute el siguiente comando para agregar el proyecto de aplicación a la solución:

    dotnet sln add AppWithPlugin/AppWithPlugin.csproj
    

Ahora podemos rellenar el esqueleto de nuestra aplicación. Reemplace el código del archivo AppWithPlugin/Program.cs por el código siguiente:

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

Creación de las interfaces del complemento

El siguiente paso para crear una aplicación con complementos es definir la interfaz que deben implementar los complementos. Se recomienda crear una biblioteca de clases que contenga cualquier tipo que planee usar para comunicarse entre la aplicación y los complementos. Esta división le permite publicar la interfaz del complemento como un paquete sin tener que enviar la aplicación completa.

En la carpeta raíz del proyecto, ejecute dotnet new classlib -o PluginBase. Además, ejecute dotnet sln add PluginBase/PluginBase.csproj para agregar el proyecto al archivo de solución. Elimine el PluginBase/Class1.cs archivo y cree un nuevo archivo en la PluginBase carpeta denominada ICommand.cs con la siguiente definición de interfaz:

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

        int Execute();
    }
}

Esta ICommand interfaz es la interfaz que implementarán todos los complementos.

Ahora que la ICommand interfaz ya está definida, se puede detallar un poco más el proyecto de la aplicación. Agregue una referencia del AppWithPlugin proyecto al PluginBase proyecto con el dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj comando desde la carpeta raíz.

Reemplace el // Load commands from plugins comentario por el siguiente fragmento de código para habilitarlo para cargar complementos desde rutas de acceso de archivo dadas:

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

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

A continuación, reemplace el // Output the loaded commands comentario por el siguiente fragmento de código:

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

Reemplace el // Execute the command with the name passed as an argument comentario por el siguiente fragmento de código:

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

command.Execute();

Y, por último, agregue métodos estáticos a la Program clase denominada LoadPlugin y CreateCommands, como se muestra aquí:

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

Carga de complementos

Ahora la aplicación puede cargar correctamente e instanciar comandos desde ensamblados de complementos cargados, pero todavía no puede cargar los ensamblados de complementos. Cree un archivo denominado PluginLoadContext.cs en la carpeta AppWithPlugin con el siguiente contenido:

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

El PluginLoadContext tipo deriva de AssemblyLoadContext. El AssemblyLoadContext tipo es un tipo especial durante la ejecución que permite a los desarrolladores aislar los ensamblados cargados en diferentes grupos, asegurando que las versiones de ensamblados no entren en conflicto. Además, un AssemblyLoadContext personalizado puede elegir diferentes rutas de acceso para cargar ensamblados y anular el comportamiento predeterminado. PluginLoadContext usa una instancia del tipo AssemblyDependencyResolver introducido en .NET Core 3.0 para resolver nombres de ensamblado en rutas de acceso. El AssemblyDependencyResolver objeto se construye con la ruta de acceso a una biblioteca de clases de .NET. Resuelve ensamblados y bibliotecas nativas mediante sus rutas de acceso relativas, basándose en el archivo .deps.json de la biblioteca de clases cuya ruta se pasó al constructor. El personalizado AssemblyLoadContext permite a los complementos tener sus propias dependencias y AssemblyDependencyResolver facilita la carga correcta de las dependencias.

Ahora que el AppWithPlugin proyecto tiene el PluginLoadContext tipo , actualice el Program.LoadPlugin método con el cuerpo siguiente:

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

Mediante el uso de una instancia diferente PluginLoadContext para cada complemento, los complementos pueden tener dependencias diferentes o incluso en conflicto sin problemas.

Complemento simple sin dependencias

De nuevo en la carpeta raíz, haga lo siguiente:

  1. Ejecute el siguiente comando para crear un proyecto de biblioteca de clases denominado HelloPlugin:

    dotnet new classlib -o HelloPlugin
    
  2. Ejecute el siguiente comando para agregar el proyecto a la AppWithPlugin solución:

    dotnet sln add HelloPlugin/HelloPlugin.csproj
    
  3. Reemplace el archivo HelloPlugin/Class1.cs por un archivo denominado HelloCommand.cs por el siguiente contenido:

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

Ahora, abra el archivo HelloPlugin.csproj . Debería tener un aspecto similar al siguiente:

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

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

</Project>

Entre las <PropertyGroup> etiquetas, agregue el siguiente elemento:

  <EnableDynamicLoading>true</EnableDynamicLoading>

<EnableDynamicLoading>true</EnableDynamicLoading> prepara el proyecto para que se pueda usar como complemento. Entre otras cosas, esto copiará todas sus dependencias al resultado del proyecto. Para obtener más información, consulte EnableDynamicLoading.

Entre las <Project> etiquetas, agregue los siguientes elementos:

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

El <Private>false</Private> elemento es importante. Esto indica a MSBuild que no copie PluginBase.dll en el directorio de salida de HelloPlugin. Si el ensamblado PluginBase.dll está presente en el directorio de salida, PluginLoadContext encontrará el ensamblado allí y lo cargará cuando cargue el ensamblado HelloPlugin.dll . En este punto, el HelloPlugin.HelloCommand tipo implementará la ICommand interfaz desde el PluginBase.dll en el directorio de salida del HelloPlugin proyecto, no en la ICommand interfaz que se carga en el contexto de carga predeterminado. Dado que el tiempo de ejecución ve estos dos tipos como tipos diferentes de ensamblados diferentes, el AppWithPlugin.Program.CreateCommands método no encontrará los comandos. Como resultado, los <Private>false</Private> metadatos son necesarios para la referencia al ensamblado que contiene las interfaces del complemento.

Del mismo modo, el <ExcludeAssets>runtime</ExcludeAssets> elemento también es importante si hace PluginBase referencia a otros paquetes. Esta configuración tiene el mismo efecto que <Private>false</Private>, pero funciona en las referencias de paquete que el proyecto PluginBase o una de sus dependencias pueden incluir.

Ahora que el HelloPlugin proyecto está completo, debe actualizar el AppWithPlugin proyecto para saber dónde se puede encontrar el HelloPlugin complemento. Después del // Paths to plugins to load comentario, agregue @"HelloPlugin\bin\Debug\net10.0\HelloPlugin.dll" (esta ruta podría ser diferente en función de la versión de .NET Core que use) como elemento del pluginPaths array.

Complemento con dependencias de biblioteca

Casi todos los complementos son más complejos que un simple "Hola mundo", y muchos complementos tienen dependencias en otras bibliotecas. Los proyectos JsonPlugin y OldJsonPlugin del ejemplo muestran dos casos de complementos con dependencias de paquetes NuGet sobre Newtonsoft.Json. Por este motivo, todos los proyectos de complemento deben agregar <EnableDynamicLoading>true</EnableDynamicLoading> a las propiedades del proyecto para copiar todas sus dependencias en la salida de dotnet build. La publicación de la biblioteca de clases con dotnet publish también copiará todas sus dependencias en la salida de publicación.

Otros ejemplos del ejemplo

El código fuente completo de este tutorial se puede encontrar en el repositorio dotnet/samples. El ejemplo completado incluye algunos otros ejemplos de AssemblyDependencyResolver comportamiento. Por ejemplo, el AssemblyDependencyResolver objeto también puede resolver bibliotecas nativas, así como ensamblados satélite localizados incluidos en paquetes NuGet. El repositorio de ejemplos con UVPlugin y FrenchPlugin demuestra estos escenarios.

Hacer referencia a una interfaz de complemento desde un paquete NuGet

Supongamos que hay una aplicación A que tiene una interfaz de complemento definida en el paquete NuGet denominado A.PluginBase. ¿Cómo hace referencia al paquete correctamente en el proyecto de complemento? En el caso de las referencias de proyecto, el uso del metadato <Private>false</Private> en el elemento ProjectReference del archivo de proyecto impedía que el archivo dll se copiara en la salida.

Para hacer referencia correctamente al A.PluginBase paquete, quiere cambiar el <PackageReference> elemento del archivo de proyecto a lo siguiente:

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

Esto evita que los A.PluginBase ensamblados se copien en el directorio de salida del complemento y garantiza que el complemento usará la versión de A de A.PluginBase.

Recomendaciones del marco de destino del complemento

Dado que la carga de dependencias del complemento usa el archivo .deps.json , hay una gotcha relacionada con el marco de destino del complemento. En concreto, los complementos deben tener como destino un entorno de ejecución, como .NET 10, en lugar de una versión de .NET Standard. El archivo .deps.json se genera en función del marco de trabajo que tiene como destino el proyecto y, dado que muchos paquetes compatibles con .NET Standard envían ensamblados de referencia para compilar en ensamblados de implementación y .NET Standard para entornos de ejecución específicos, es posible que el .deps.json no vea correctamente los ensamblados de implementación, o puede capturar la versión de .NET Standard de un ensamblado en lugar de la versión de .NET Core que espera.

Referencias del marco de complementos

Actualmente, los complementos no pueden introducir nuevos marcos en el proceso. Por ejemplo, no se puede cargar un complemento que use el Microsoft.AspNetCore.App marco en una aplicación que solo use el marco raíz Microsoft.NETCore.App . La aplicación host debe declarar referencias a todos los marcos necesarios para los complementos.