Încărcarea dinamică a asamblărilor și legarea târzie
La crearea unei aplicații, se definește un set de asamblări care vor fi utilizate. În proiect se specifică referințele către aceste asamblări, iar când aplicația este executată, la accesarea funcționalității acestor asamblări, acestea sunt încărcate automat.
Totuși, putem încărca dinamic și alte asamblări, pentru care nu există referințe în proiect.
Pentru gestionarea asamblărilor, în spațiul de nume System.Reflection există clasa Assembly. Cu ajutorul acesteia, putem încărca și examina o asamblare.
Pentru a încărca dinamic o asamblare în aplicație, trebuie să folosim metodele statice Assembly.LoadFrom() sau Assembly.Load().
Metoda LoadFrom() primește ca parametru calea către asamblare.
Să presupunem că avem două proiecte:

Să presupunem că în proiectul MyApp, care este compilat într-o asamblare MyApp.dll, avem un fișier Program.cs cu următorul cod:
Person tom = new Person("Tom");
Console.WriteLine($"Hello, {tom.Name}");
class Person
{
public string Name { get; }
public Person(string name) => Name = name;
}
În alt proiect, vom examina asamblarea MyApp.dll pentru a vedea ce tipuri conține:
using System.Reflection;
string dllPath = @"C:\Users\Petrea\source\repos\MyApp\MyApp\bin\Debug\net8.0\MyApp.dll";
Assembly asm = Assembly.LoadFrom(dllPath);
Console.WriteLine(asm.FullName);
// obținem toate tipurile din asamblarea MyApp.dll
Type[] types = asm.GetTypes();
foreach (Type t in types)
{
Console.WriteLine(t.Name);
}
În acest caz, se specifică asamblarea MyApp.dll pentru examinare. Aici este folosită calea relativă, deoarece asamblarea se află în același folder cu aplicația - în proiect, în directorul bin/Debug/net8.0. Se poate specifica și numele aplicației curente, caz în care programul se va examina pe sine. Oricum, trebuie avut în vedere că se pot încărca (cel puțin în .NET 8.0) asamblări cu extensia .dll, dar nu și .exe.
În cazul meu, voi obține următoarea ieșire în consolă:
MyApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Program
Person
După cum se poate observa din ieșire, denumirea completă a asamblării este: MyApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null. Iar asamblarea MyApp.dll conține patru tipuri - pe lângă clasa Person și clasa Program definită implicit.
Metoda Load() funcționează similar, doar că primește ca parametru un nume prietenos al asamblării, care deseori coincide cu numele aplicației: Assembly asm = Assembly.Load("MyApp");
După ce am obținut toate tipurile asamblării cu ajutorul metodei GetTypes(), putem aplica fiecărui tip toate metodele discutate în tema anterioară.
Legarea târzie
Cu ajutorul încărcării dinamice, putem implementa tehnologia legării târzii. Legarea târzie permite crearea instanțelor unui anumit tip și utilizarea acestora în timpul execuției aplicației.
Utilizarea legării târzii este mai puțin sigură, deoarece în cazul codificării stricte a tuturor tipurilor (legare timpurie) la etapa de compilare, putem detecta multe erori. Totuși, legarea târzie permite crearea aplicațiilor extensibile, unde funcționalitatea suplimentară a programului nu este cunoscută și poate fi dezvoltată și conectată de dezvoltatori terți.
Rolul cheie în legarea târzie îl joacă clasa System.Activator. Cu ajutorul metodei sale statice Activator.CreateInstance(), se pot crea instanțe ale unui tip specificat.
De exemplu, să încărcăm dinamic o asamblare și să apelăm o metodă din aceasta. Să presupunem că asamblarea MyApp.exe conține următorul program:
class Program
{
static void Main(string[] args)
{
var number = 5;
var result = Square(number);
Console.WriteLine($"Pătratul lui {number} este {result}");
}
static int Square(int n) => n * n;
}
În acest caz, am definit explicit clasa Program cu metoda Main. De asemenea, în clasa Program am definit o metodă statică Square, care primește un număr ca parametru și returnează pătratul acestuia.
Acum, să încărcăm dinamic asamblarea cu acest program într-un alt program și să apelăm metodele sale.
Să presupunem că programul nostru principal arată astfel:
using System.Reflection;
Assembly asm = Assembly.LoadFrom("MyApp.dll");
Type? t = asm.GetType("Program");
if (t is not null)
{
// obținem metoda Square
MethodInfo? square = t.GetMethod("Square", BindingFlags.NonPublic | BindingFlags.Static);
// apelăm metoda, îi transmitem valori pentru parametri și obținem rezultatul
object? result = square?.Invoke(null, new object[] { 7 });
Console.WriteLine(result); // 49
}
Mai întâi obținem o referință la asamblarea pe care o examinăm în variabila asm:
Assembly asm = Assembly.LoadFrom("MyApp.dll");
Apoi, cu ajutorul metodei GetType, obținem tipul - clasa Program, care se află în asamblarea MyApp.dll:
Type? t = asm.GetType("Program");
În cele din urmă, apelăm metoda. În primul rând, obținem metoda propriu-zisă:
MethodInfo? square = t.GetMethod("Square", BindingFlags.NonPublic | BindingFlags.Static);
Deoarece metoda Square este privată și statică, se transmit flagurile BindingFlags.NonPublic | BindingFlags.Static ca al doilea parametru al metodei.
Apoi, apelăm metoda folosind Invoke:
object? result = square?.Invoke(null, new object[] { 7 });
Aici, primul parametru reprezintă obiectul pentru care este apelată metoda, iar al doilea - setul de parametri sub forma unei matrice object[]. Totuși, deoarece metoda apelată este statică și nu aparține unui anumit obiect, se transmite null ca prim argument în metodă.
Deoarece metoda Square returnează o valoare, putem obține această valoare din metodă sub forma unui obiect de tip object.
Dacă metoda nu ar fi primit parametri, în locul matricei de obiecte s-ar fi folosit valoarea null: method.Invoke(null, null).
În asamblarea MyApp.exe, în clasa Program există și o altă metodă - metoda Main, care de asemenea efectuează anumite operațiuni. Să o apelăm acum:
using System.Reflection;
Assembly asm = Assembly.LoadFrom("MyApp.dll");
Type? program = asm.GetType("Program");
if (program is not null)
{
// obținem metoda Main
MethodInfo? main = program.GetMethod("Main", BindingFlags.NonPublic | BindingFlags.Static);
// apelăm metoda Main
main?.Invoke(null, new object[] { new string[] { } }); // Pătratul lui 5 este 25
}
Deoarece metoda Main este statică și non-publică, se aplică și aici masca de biți BindingFlags.NonPublic | BindingFlags.Static. Și deoarece ea primește ca parametru un șir de stringuri, la apelarea metodei se transmite o valoare corespunzătoare: main.Invoke(null, new object[]{new string[]{}}).