Informazioni su System.Runtime.Loader.AssemblyLoadContext

La classe AssemblyLoadContext è stata introdotta in .NET Core e non è disponibile in .NET Framework. Questo articolo integra la documentazione dell'API AssemblyLoadContext con informazioni concettuali.

Questo articolo è rilevante per gli sviluppatori che implementano il caricamento dinamico, in particolare per sviluppatori di framework con caricamento dinamico.

Che cos'è AssemblyLoadContext?

Ogni applicazione .NET 5+ e .NET Core usa in modo implicito AssemblyLoadContext. Si tratta del provider del runtime per l'individuazione e il caricamento delle dipendenze. Ogni volta che viene caricata una dipendenza, viene richiamata un'istanza AssemblyLoadContext per individuarla.

  • AssemblyLoadContext offre un servizio di individuazione, caricamento e memorizzazione nella cache di assembly gestite e altre dipendenze.
  • Per supportare il caricamento e lo scaricamento di codice dinamico, crea un contesto isolato per il caricamento del codice e delle relative dipendenze nella propria istanza di AssemblyLoadContext.

Regole di controllo delle versioni

Una singola istanza di AssemblyLoadContext è limitata al caricamento di una sola versione di un Assembly per nome di assembly semplice. Quando un riferimento all'assembly viene risolto in un'istanza di AssemblyLoadContext che dispone già di un assembly di tale nome caricato, la versione richiesta viene confrontata con la versione caricata. La risoluzione avrà esito positivo solo se la versione caricata è uguale o successiva alla versione richiesta.

Quando sono necessarie più istanze di AssemblyLoadContext?

La restrizione per cui una singola istanza di AssemblyLoadContext può caricare una sola versione di un assembly può diventare un problema durante il caricamento dinamico dei moduli di codice. Ogni modulo viene compilato in modo indipendente e i moduli possono dipendere da versioni diverse di un oggetto Assembly. Questo è spesso un problema quando moduli diversi dipendono da versioni diverse di una libreria di uso comune.

Per supportare il caricamento dinamico del codice, l'API AssemblyLoadContext consente di caricare versioni in conflitto di un Assembly nella stessa applicazione. Ogni istanza di AssemblyLoadContext fornisce un dizionario univoco che esegue il mapping di ogni AssemblyName.Name a un'istanza di Assembly specifica.

Fornisce anche un meccanismo pratico per raggruppare le dipendenze correlate a un modulo di codice per un successivo scaricamento.

Istanza di AssemblyLoadContext.Default

L'istanza AssemblyLoadContext.Default viene popolata automaticamente dal runtime all'avvio. Usa probe predefinito per individuare e trovare tutte le dipendenze statiche.

Risolve gli scenari di caricamento delle dipendenze più comuni.

Dipendenze dinamiche

AssemblyLoadContext include vari eventi e funzioni virtuali che possono essere sottoposte a override.

L'istanza AssemblyLoadContext.Default supporta solo l'override degli eventi.

Gli articoli algoritmo di caricamento di assembly gestiti, algoritmo di caricamento di assembly satellite e algoritmo di caricamento della libreria non gestita (nativa) fanno riferimento a tutti gli eventi e le funzioni virtuali disponibili. Negli articoli viene mostrata la posizione relativa di ogni evento e funzione negli algoritmi di caricamento. Questo articolo non riproduce tali informazioni.

Questa sezione illustra i principi generali per gli eventi e le funzioni pertinenti.

  • Essere ripetibile. Una query per una dipendenza specifica deve sempre generare la stessa risposta. È necessario restituire la stessa istanza di dipendenza caricata. Questo requisito è fondamentale per la coerenza della cache. Per gli assembly gestiti in particolare, viene creata una cache Assembly. La chiave della cache è un nome di assembly semplice, AssemblyName.Name.
  • in genere non generano. È previsto che queste funzioni restituiscano null anziché generare quando non è possibile trovare la dipendenza richiesta. La generazione terminerà prematuramente la ricerca e propagherà un'eccezione al chiamante. La creazione di un'eccezione deve essere limitata a errori imprevisti, ad esempio un assembly danneggiato o una condizione di memoria insufficiente.
  • Evitare ricorsione. Tenere presente che queste funzioni e gestori implementano le regole di caricamento per individuare le dipendenze. L'implementazione non deve chiamare API che attivano la ricorsione. Il codice deve in genere chiamare funzioni di caricamento AssemblyLoadContext che richiedono un percorso o un argomento di riferimento alla memoria specifico.
  • Caricare nell’AssemblyLoadContext corretto. La scelta di dove caricare le dipendenze è specifica dell'applicazione. La scelta viene implementata da questi eventi e funzioni. Quando il codice chiama le funzioni load-by-path AssemblyLoadContext, le chiama nell'istanza in cui si vuole caricare il codice. In alcuni casi, la restituzione di null e la gestione del carico da parte di AssemblyLoadContext.Default può essere l'opzione più semplice.
  • Essere consapevoli delle razze di thread. Il caricamento può essere attivato da più thread. AssemblyLoadContext gestisce le gare dei thread aggiungendo in modo atomico gli assembly alla cache. L'istanza del perdente della gara viene eliminata. Nella logica di implementazione, non aggiungere logica aggiuntiva che non gestisca correttamente più thread.

In che modo le dipendenze dinamiche sono isolate?

Ogni istanza di AssemblyLoadContext rappresenta un ambito univoco per le istanze di Assembly e le definizioni di Type.

Non esiste alcun isolamento binario tra queste dipendenze. Sono isolati solo perché non si trovano per nome.

In ogni AssemblyLoadContext:

Dipendenze condivise

Le dipendenze possono essere facilmente condivise tra istanze di AssemblyLoadContext. Il modello generale prevede che AssemblyLoadContext carichi una dipendenza. L'altro condivide la dipendenza usando un riferimento all'assembly caricato.

Questa condivisione è necessaria per gli assembly di runtime. Questi assembly possono essere caricati solo in AssemblyLoadContext.Default. Lo stesso è richiesto per framework come ASP.NET, WPF o WinForms.

È consigliabile caricare le dipendenze condivise in AssemblyLoadContext.Default. Questa condivisione è il modello di progettazione comune.

La condivisione viene implementata nella codifica dell'istanza personalizzata AssemblyLoadContext. AssemblyLoadContext include vari eventi e funzioni virtuali che possono essere sottoposte a override. Quando una di queste funzioni restituisce un riferimento a un'istanza di Assembly caricata in un'altra istanza di AssemblyLoadContext, l'istanza di Assembly viene condivisa. L'algoritmo di caricamento standard rimanda a AssemblyLoadContext.Default per il caricamento per semplificare il modello di condivisione comune. Per altre informazioni, vedere algoritmo di caricamento dell'assembly gestito.

Problemi di conversione dei tipi

Quando due istanze di AssemblyLoadContext contengono definizioni di tipo con lo stesso name, non sono dello stesso tipo. Sono dello stesso tipo se e solo se provengono dalla stessa istanza di Assembly.

Per complicare le cose, i messaggi di eccezione relativi a questi tipi non corrispondenti possono generare confusione. I tipi vengono indicati nei messaggi di eccezione in base ai nomi dei tipi semplici. Il messaggio di eccezione comune in questo caso è nel formato seguente:

Impossibile convertire l'oggetto di tipo 'IsolatedType' nel tipo 'IsolatedType'.

Problemi di conversione dei tipi di debug

Data una coppia di tipi non corrispondenti, è importante conoscere anche:

Dati due oggetti a e b, sarà utile la valutazione di quanto segue nel debugger:

// In debugger look at each assembly's instance, Location, and FullName
a.GetType().Assembly
b.GetType().Assembly
// In debugger look at each AssemblyLoadContext's instance and name
System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(a.GetType().Assembly)
System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(b.GetType().Assembly)

Risolvere i problemi di conversione dei tipi

Esistono due modelli di progettazione per risolvere questi problemi di conversione dei tipi.

  1. Usare tipi condivisi comuni. Questo tipo condiviso può essere un tipo di runtime primitivo oppure può comportare la creazione di un nuovo tipo condiviso in un assembly condiviso. Spesso il tipo condiviso è un'interfaccia definita in un assembly dell'applicazione. Per altre informazioni, vedere come vengono condivise le dipendenze.

  2. Usare tecniche di marshalling per eseguire la conversione da un tipo a un altro.