Динамическая загрузка сборок и позднее связывание
При создании приложения для него определяется набор сборок, которые будут использоваться. В проекте указываются ссылки на эти сборки, и когда приложение выполняется, при обращении к функционалу этих сборок они автоматически подгружаются.
Но также мы можем сами динамически подгружать другие сборки, на которые в проекте нет ссылок.
Для управления сборками в пространстве имен System.Reflection имеется класс Assembly. С его помощью можно загружать сборку, исследовать ее.
Чтобы динамически загрузить сборку в приложение, надо использовать статические методы Assembly.LoadFrom() или Assembly.Load().
Метод LoadFrom() принимает в качестве параметра путь к сборке.
Допустим, у нас есть два проекта:
Загрузка сборок в C# и .NET
Пусть в проекте MyApp, который компилируется в сборку MyApp.dll, имеется файл Program.cs со следующим кодом:
Person tom = new Person("Tom");
Console.WriteLine($"Hello, {tom.Name}");
class Person
{
public string Name { get; }
public Person(string name) => Name = name;
}
В другом проект исследуем сборку MyApp.dll на наличие в ней различных типов:
using System.Reflection;
Assembly asm = Assembly.LoadFrom("MyApp.dll");
Console.WriteLine(asm.FullName);
// получаем все типы из сборки MyApp.dll
Type[] types = asm.GetTypes();
foreach (Type t in types)
{
Console.WriteLine(t.Name);
}
В данном случае для исследования указывается сборка MyApp.dll. Здесь использован относительный путь, так как сборка находится в одной папке с приложением - в проекте в каталоге bin/Debug/net6.x. Можно в принципе в качестве имени указать и имя текущего приложение. В этом случае программа будет исследовать саму себя. В любом случае стоит учитывать, что загрузке подлежат (по крайней мере в .NET 6.0) сборки с расширением dll, но не exe.
И в моем случае я получу следующий консольный вывод:
MyApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
EmbeddedAttribute
NullableAttribute
NullableContextAttribute
Program
Person
Как видно из вывода, полное название сборки: MyApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null. А сама сборка MyApp.dll содержит пять типов - кроме класса Person и неявно определяемого класса Program добавляется еще три автоматически генерируемых класса.
Метод Load() действует аналогично, только в качестве его параметра передается дружественное имя сборки, которое нередко совпадает с именем приложения: Assembly asm = Assembly.Load("MyApp");
Получив все типы сборки с помощью метода GetTypes(), мы опять же можем применить к каждому типу все те методы, которые были рассмотрены в прошлой теме.
Позднее связывание
С помощью динамической загрузки мы можем реализовать технологию позднего связывания. Позднее связывание позволяет создавать экземпляры некоторого типа, а также использовать его во время выполнения приложения.
Использование позднего связывания менее безопасно в том плане, что при жестком кодировании всех типов (ранее связывание) на этапе компиляции мы можем отследить многие ошибки. В то же время позднее связывание позволяет создавать расширяемые приложения, когда дополнительный функционал программы неизвестен, и его могут разработать и подключить сторонние разработчики.
Ключевую роль в позднем связывании играет класс System.Activator. С помощью его статического метода Activator.CreateInstance() можно создавать экземпляры заданного типа.
Например, динамически загрузим сборку и вызовем у ней некоторый метод. Допустим, загружаемая сборка MyApp.exe представляет следующую программу:
class Program
{
static void Main(string[] args)
{
var number = 5;
var result = Square(number);
Console.WriteLine($"Квадрат {number} равен {result}");
}
static int Square(int n) => n * n;
}
В данном случае мы явным образом определили класс Program с методом Main. И кроме того, в классе Program определен статический метод Square, который в качестве параметра принимает число и возвращает его квадрат.
Теперь динамически подключим сборку с этой программой в другой программе и вызовем ее методы.
Пусть наша основная программа будет выглядеть так:
using System.Reflection;
Assembly asm = Assembly.LoadFrom("MyApp.dll");
Type? t = asm.GetType("Program");
if (t is not null)
{
// создаем экземпляр класса Program
object? obj = Activator.CreateInstance(t);
// получаем метод Square
MethodInfo? square = t.GetMethod("Square", BindingFlags.NonPublic | BindingFlags.Static);
// вызываем метод, передаем ему значения для параметров и получаем результат
object? result = square?.Invoke(obj, new object[] { 7 });
Console.WriteLine(result); // 49
}
Сначала получаем ссылку на исследуемую сборку в переменную asm:
Assembly asm = Assembly.LoadFrom("MyApp.dll")
Затем с помощью метода GetType получаем тип - класс Program, который находится в сборке MyApp.dll:
Type? t = asm.GetType("Program");
Получив тип, создаем его экземпляр:
object? obj = Activator.CreateInstance(t)
Результат создания - объект класса Program представляет собой переменную obj.
И в конце остается вызвать метод. Во-первых, получаем сам метод:
MethodInfo? square = t.GetMethod("Square", BindingFlags.NonPublic | BindingFlags.Static);
Поскольку метод Square приватный и статический, то в качестве второго параметра в метод передаются флаги BindingFlags.NonPublic | BindingFlags.Static
И потом с помощью метода Invoke вызываем его:
object? result = square?.Invoke(obj, new object[] { 7 });
Здесь первый параметр представляет объект, для которого вызывается метод, а второй - набор параметров в виде массива object[].
Так как метод Square возвращает некоторое значение, то мы можем его получить из метода в виде объекта типа object.
Если бы метод не принимал параметров, то вместо массива объектов использовалось бы значение null: method.Invoke(obj, null)
В сборке MyApp.exe в классе Program также есть и другой метод - метод Main, который также выполняет некоторую работу. Вызовем теперь его:
using System.Reflection;
Assembly asm = Assembly.LoadFrom("MyApp.dll");
Type? program = asm.GetType("Program");
if (program is not null)
{
// создаем экземпляр класса Program
object? obj = Activator.CreateInstance(program);
// получаем метод Main
MethodInfo? main = program.GetMethod("Main", BindingFlags.NonPublic | BindingFlags.Static);
// вызываем метод Main
main?.Invoke(obj, new object[] { new string[] { } }); // Квадрат 5 равен 25
}
Так как метод Main является статическим и не публичным, то к нему также применяется битовая маска BindingFlags.NonPublic | BindingFlags.Static. И поскольку он в качестве параметра принимает массив строк, то при вызове метода передается соответствующее значение: main.Invoke(obj, new object[]{new string[]{}})