Как загрузить сборки, расположенные в папке в .net-приложении консоли ядра

Я создаю консольное приложение на платформе .Net Core и задаюсь вопросом, как загружать сборки (файлы DLL) и создавать экземпляры классов с использованием динамических функций С#? Кажется, это сильно отличается от .Net 4.X, и он не документирован...

Например, скажем, у меня есть библиотека классов (.Net Core), и у нее есть только один класс:

namespace MyClassLib.SampleClasses
{
    public class Sample
    {
        public string SayHello(string name)
        {
            return $"Hello {name}";
        }

        public DateTime SayDateTime()
        {
            return DateTime.Now;
        }
    }
}

Таким образом, имя файла dll будет MyClassLib.dll и его расположение находится в /dlls/MyClassLib.dll.

Теперь я хочу загрузить это в простое консольное приложение (.Net Core) и создать экземпляр класса Sample и вызвать методы с использованием динамических функций С# в следующем консольном приложении:

namespace AssemblyLoadingDynamic
{
    public class Program
    {
        public static void Main(string[] args)
        {
            // load the assembly and use the classes
        }
    }
}

Примечание: По .Net Core я имею в виду версию RC2.

Ответы

Ответ 1

Не уверен, что это лучший способ сделать это, но вот что я придумал:

(Проверено только на .Net Core RC2 - Windows)

using System;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.DependencyModel;

namespace AssemblyLoadingDynamic
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var asl = new AssemblyLoader();
            var asm = asl.LoadFromAssemblyPath(@"C:\Location\Of\" + "SampleClassLib.dll");

            var type = asm.GetType("MyClassLib.SampleClasses.Sample");
            dynamic obj = Activator.CreateInstance(type);
            Console.WriteLine(obj.SayHello("John Doe"));
        }

        public class AssemblyLoader : AssemblyLoadContext
        {
            // Not exactly sure about this
            protected override Assembly Load(AssemblyName assemblyName)
            {
                var deps = DependencyContext.Default;
                var res = deps.CompileLibraries.Where(d => d.Name.Contains(assemblyName.Name)).ToList();
                var assembly = Assembly.Load(new AssemblyName(res.First().Name));
                return assembly;
            }
        }
    }
}

MyClassLib.SampleClasses - это пространство имен, а Sample - это имя класса aka.

При выполнении, он попытается загрузить SampleClassLib.dll скомпилированную библиотеку классов в память и даст моему консольному приложению доступ к MyClassLib.SampleClasses.Sample (посмотрите на вопрос), тогда мое приложение вызывает метод SayHello() и передает "John Doe" в качестве названия, поэтому программа печатает:

"Hello John Doe"

Быстрое примечание: Переопределение для метода Load не имеет значения, поэтому вы можете просто заменить его содержимое на throw new NotImplementedException(), и оно не должно влиять на все, о чем мы заботимся.

Ответ 2

В настоящее время работает против netcoreapp1.0, вам на самом деле не нужно идти в меру реализации своего собственного AssemblyLoader. Существует Default, который работает просто отлично. (Следовательно, @VSG24 упоминает, что Load ничего не делает).

using System;
using System.Runtime.Loader;

namespace AssemblyLoadingDynamic
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var myAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(@"C:\MyDirectory\bin\Custom.Thing.dll");
            var myType = myAssembly.GetType("Custom.Thing.SampleClass");
            var myInstance = Activator.CreateInstance(myType);
        }
    }   
}

с project.json похожим на:

{
  "version": "1.0.0-*",
  "buildOptions": {
    "emitEntryPoint": true
  },

  "dependencies": {
    "Microsoft.NETCore.App": {
      "type": "platform",
      "version": "1.0.1"
    },
    "System.Runtime.Loader": "4.0.0"
  },

  "frameworks": {
    "netcoreapp1.0": {
      "imports": "dnxcore50"
    }
  }
}

Ответ 3

Спасибо за ваш обмен. Он также работает с Net Core 1.0. Если вашей сборке нужны другие сборки на одном пути, вы можете использовать образец кода ниже.

using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.DependencyModel;
public class AssemblyLoader : AssemblyLoadContext
{
    private string folderPath;

    public AssemblyLoader(string folderPath)
    {
        this.folderPath = folderPath;
    }

    protected override Assembly Load(AssemblyName assemblyName)
    {
        var deps = DependencyContext.Default;
        var res = deps.CompileLibraries.Where(d => d.Name.Contains(assemblyName.Name)).ToList();
        if (res.Count > 0)
        {
            return Assembly.Load(new AssemblyName(res.First().Name));
        }
        else
        {
            var apiApplicationFileInfo = new FileInfo($"{folderPath}{Path.DirectorySeparatorChar}{assemblyName.Name}.dll");
            if (File.Exists(apiApplicationFileInfo.FullName))
            {
                var asl = new AssemblyLoader(apiApplicationFileInfo.DirectoryName);
                return asl.LoadFromAssemblyPath(apiApplicationFileInfo.FullName);
            }
        }
        return Assembly.Load(assemblyName);
    }
}

Не забудьте добавить следующие зависимости в ваш файл project.json:

 "System.Runtime.Loader"
 "Microsoft.Extensions.DependencyModel"

Ответ 4

Используя .net core 1.1/standard 1.6, я обнаружил, что AssemblyLoader недоступен, а

AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath)
дал мне ошибку "Не удалось загрузить файл или сборку xxx".

Наконец, это решение ниже работало для меня - просто добавив шаг для получения объекта AssemblyName. Надеюсь, это поможет любому, кто застрял:

var assemblyName = AssemblyLoadContext.GetAssemblyName(assemblyPath);
var assembly = Assembly.Load(assemblyName);

Ответ 5

@Rob, единственный способ, которым я мог бы построить ваш пример, - это изменить тип myInstance на динамический.

Оставляя тип var, он позволяет создавать код, но как только я попытаюсь использовать метод из загруженной сборки во время выполнения, я получаю ошибки компилятора, такие как myInstance, не содержит метода X. Я новичок в этом, но разметка типа динамическая, кажется, имеет смысл. Если тип загружается во время выполнения, то как проверить компилятор myInstance будет содержать метод X или prop Y? Набрав myInstance как динамический, я считаю, что вы удаляете проверку компилятора, и, таким образом, я мог бы получить пример для создания и запуска просто отлично. Не уверен, что это на 100% правильный путь (я не знаю достаточно, и вы можете посоветовать, что там проблема с использованием динамического?), Но это единственный способ заставить меня работать, не прибегая к проблемам создания собственных AssemblyLoader (как вы правильно указываете).

Итак...

using System;
using System.Runtime.Loader;

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var myAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(@"C:\Documents\Visual Studio 2017\Projects\Foo\Foo\bin\Debug\netcoreapp2.0\Foo.dll");
            var myType = myAssembly.GetType("Foo.FooClass");
            dynamic myInstance = Activator.CreateInstance(myType);
            myInstance.UpperName("test");
        }
    }
}

Надеюсь, что это помогает кому-то быть новым, мне потребовались годы, чтобы точно определить, почему myInstance как var не имел метода X и т.д. Doh!

Ответ 6

Я много в этом разбираюсь, пробовал подход DependencyContext... Он хорошо работает, но имеет некоторые ограничения и отличается от стандартного разрешения сборки, которое есть в приложении dotnet c++, которое запускает ваше приложение. Вы должны выполнить сопоставление имен вручную, и если ваше хост-приложение опубликовано, у вас не будет пути поиска для папки nuget, которая является проблемой (решаемой), если ваша дочерняя сборка находится в режиме отладки и использует nuget...

Итак, вот еще одно решение: если приложение (assemblyA), загружающее сборку вручную (assemblyB), не имеет зависимостей (или не имеет конфликтующих зависимостей с assemblyB), я предлагаю обмануть и по умолчанию использовать разрешение сборки AssemblyB. Для dotnet.exe есть скрытый гем, который позволяет вам загрузить файл deps по вашему выбору, чтобы вы могли сделать что-то вроде этого:

dotnet exec --depsfile pathToAssemblyB\assemblyB.deps.json --runtimeconfig pathToAssemblyB\assemblyB.runtimeconfig.json AssemblyA.dll

и затем вы можете загрузить сборку, как описано в других ответах, с помощью

var myAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath("pathToAssemblyB\\AssemblyB.dll");

Таким образом, он корректно разрешит все зависимости для AssemblyB, но не для AssemblyA. Это обратная проблема, но если у вас есть небольшое приложение, которое хочет сделать удаленное взаимодействие в большом приложении, это полезно. Другая проблема заключается в том, что вам нужно знать, что вы собираетесь использовать ассемблер B при запуске приложения и что оно работает только один раз за выполнение. Таким образом, существует другой набор проблем, и вы можете выбрать свой подход в зависимости от вашей ситуации. Обратите внимание, что это неподдерживаемая/недокументированная функция, но она используется в основных инструментальных средствах EF, поэтому сейчас она "жизнеспособна"...

Ответ 7

Я думаю, что это ниже будет работать для вас, и надеюсь, что это может помочь некоторым новичкам MEF2, как я.

    /// <summary>
    /// Gets the assemblies that belong to the application .exe subfolder.
    /// </summary>
    /// <returns>A list of assemblies.</returns>
    private static IEnumerable<Assembly> GetAssemblies()
    {
        string executableLocation = AppContext.BaseDirectory;
        string directoryToSearch = Path.Combine(Path.GetDirectoryName(executableLocation), "Plugins");
        foreach (string file in Directory.EnumerateFiles(directoryToSearch, "*.dll"))
        {
            Assembly assembly = null;
            try
            {
                //Load assembly using byte array
                byte[] rawAssembly = File.ReadAllBytes(file);
                assembly = Assembly.Load(rawAssembly);
            }
            catch (Exception)
            {
            }

            if (assembly != null)
            {
                yield return assembly;
            }
        }
    }

еще один, но в .netstandard1.3 ни один не был доступен.

var assembiles = Directory.GetFiles(Assembly.GetEntryAssembly().Location, "*.dll", SearchOption.TopDirectoryOnly)
        .Select(AssemblyLoadContext.Default.LoadFromAssemblyPath);