Il Framework: la Global Assembly Cache
a cura di Fabrizio Baldazzi (requisiti: conoscenza base del Framework)

Premessa
Questo articolo vuol essere d'aiuto a coloro che ancora non hanno iniziato a sviluppare in .NET, ma sono sempre rimasti fedeli a Visual Studio 6.0. Visto che il futuro è basato sullo sviluppo su questa piattaforma mi è sembrato utile per tutti cercare di chiarire alcuni concetti base.
In questo articolo approfondirò il discorso sulla GAC.
A chi fosse nuovo all'utilizzo di .NET, consiglio di leggere il mio precedente articolo sul Framework.

La GAC
Da quando Visual Studio .NET è uscito sul mercato, nel nostro vocabolario informatico sono comparsi nuovi termini, uno tra i tanti è GAC.
GAC è l'acronimo di Global Assembly Cache. Inizialmente si chiamava Fusion Cache, dal nome del progetto inerente al suo sviluppo e dalla DLL che la implementa, fusion.dll.

La GAC è un contenitore di file (Assembly) che verranno condivisi dalle varie applicazioni .NET. Nella GAC non si posso mettere tutti gli Assembly ma solo quelli che hanno Strong Name (nome sicuro) in modo tale da poter individuare in modo univoco l'Assembly che si trova all'interno della GAC stessa. Gli Strong Name sono implementati utilizzando meccanismi di crittografia a chiave pubblica (public key); questa chiave viene salvata nel campo Identità del Manifesto dell'Assembly e consente di rendere l'Assembly unico e riconoscibile; tutti gli Assemblies che faranno riferimento ad un determinato Assembly utilizzeranno la chiave pubblica per identificarlo. Una cosa interessante è che nella GAC possono essere installate più versioni dello stesso Assembly e per ciascuna versione possono esistere diverse specializzazioni, ad esempio, in base alla cultura richiesta. L'introduzione della GAC ha introdotto una notevole differenza tra .NET e COM, ossia che in quest'ultimo i componenti venivano installati in modo globale mentre in .NET questo non è più necessario in quanto gli Assembly possono essere anche privati. Naturalmente ci sono dei vantaggi ad installare un Assembly in modo condiviso, ossia:

Quando si installa un nuovo Assembly nella GAC, tramite gli strumenti adeguati, ad esempio l'utility gacutil.exe, viene eseguito un controllo della firma dell'Assembly per verificare se l'Assembly ha Strong Name. Nel caso in cui non lo abbia, il comando gacutil restituirà un errore del tipo:

  Failure adding assembly to the cache: Attempt to install an assembly without a strong name

senza inserire l'Assembly in questione nella GAC.
Come detto in precedenza, per registrare o cancellare un Assembly in GAC si può utilizzare l'utility gacutil. Ad esempio:

Per inserire un Assembly in GAC
gacutil /i AssemblyName
Per cancellare un Assembly dalla GAC
gacutil /u AssemblyName
Per visualizzare l'elenco degli Assemblies
gacutil /l AssemblyName

Vediamo in dettaglio le opzioni che possono essere passate al comando gactuil:

Opzione
Descrizione
/i
Installa un Assembly nella GAC. Include il nome del file che contiene il manifesto come parametro
/if
Installa un Assembly nella GAC e forza la sovrascrittura se l'Assembly è già presente nella cache. Include il nome del file che contiene il manifesto come parametro
/ir
Installa un Assembly nella GAC con traccia ai riferimenti. Include il nome del file contenente il manifesto, lo schema di riferimento, ID e la descrizione come parametri
/u
Disinstalla l'Assembly. Contiene come parametro il nome dell'Assembly da disinstallare
/ur
Disinstalla il riferimento a un'Assembly. Come parametri include il nome dell'Assembly, il tipo di riferimento e ID
/uf
Forza la disinstallazione di un Assembly rimuovendo tutti i riferimenti installati. Come parametro include il full name dell'Assembly. L'Assembly sarà rimosso a meno che non sia referenziato da Window Installer
/l
Fornisce l'elenco del contenuto della GAC
/lr
Fornisce l'elenco del contenuto della GAC tenendo traccia dei riferimenti
/cdl
Cancella il contenuto della cache scaricata
/ldl
Fornisce l'elenco dei file scaricati dalla cache
/nologo
Elimina la visualizzazione del logo
/silent
Elimina la visualizzazione dell'output
/?
Visualizza l'elenco delle opzioni

In pratica, con la GAC si è risolto uno dei problemi più grossi che si avevano nel mondo COM, ossia il "DLL Hell" o "Inferno delle DLL".

La GAC è rilocabile?
La GAC non è altro che una directory (di nome assembly) che si trova sul disco in:

  %windir%\assembly

Ad esempio sul mio disco il percorso completo è:

  c:\winnt\assembly

La directory assembly ha come percorso di default %windir%, ma può avere comunque una posizione diversa da questa. Questa posizione non è configurabile durante la fase di installazione. Una volta che il Framework .NET è completamente installato è possibile riposizionare la GAC. Per fare questo si deve apportare una modifica al registro di sistema, e precisamente alla chiave CacheLocation che si trova in:

  HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Fusion\CacheLocation

Probabilmente la chiave CacheLocation non sarà presente, e andrà quindi creata. Per procedere allo spostamento della GAC si deve quindi impostare la chiave di registro CacheLocation col percorso che indica dove deve essere posizionata la GAC. .NET provvederà a creare una sottodirectory all'interno del percorso specificato nella chiave di registro CacheLocation. A questo punto si dovrà copiare il contenuto della GAC attuale (che con molta probabilità sarà nella posizione di default, %windir%\assembly), nella nuova posizione. Per esempio, se voglio spostare la GAC sulla mia macchina dalla posizione di default c:\winnt\assembly a d:\myGAC, devo seguire i seguenti passi:

Non è necessario eseguire i passi precedenti nell'ordine in cui li ho scritti. Naturalmente, una volta impostata la chiave di registro con la nuova posizione della GAC, la directory assembly non sarà presente nella nuova posizione, ma comparirà solo dopo aver eseguito il comando gacutil per installare un nuovo componente nella GAC o dopo aver copiato il contenuto dalla vecchia posizione della GAC. Questa operazione è comunque piuttosto delicata e deve esserci un motivo molto valido per procedere al riposizionamento della GAC, anche perché .NET non imposta nessun tipo di sicurezza sulla nuova posizione della GAC. La posizione di default della GAC eredita l'ACL (Access Control List, che impedisce ad un Assembly di essere modificato una volta installato nella GAC) in modo tale che soltanto l'amministratore possa modificare i permessi su quella directory, quindi dopo aver rilocato la GAC è necessario impostare manualmente gli appropriati ACL alla nuova posizione della GAC.

La struttura della GAC
Un vantaggio notevole rispetto al passato, ossia al mondo COM, è che la GAC consente di mantenere installate nello stesso momento due o più versioni diverse dello stesso componente, utilizzando senza alcun tipo di modifica sia le applicazioni scritte per la prima versione del componente condiviso, sia le applicazioni scritte per una delle versioni successive. Ogni applicazione continua a usare la versione del componente per cui è stata scritta senza ricorrere ad Assembly privati. Una volta che l'Assembly è stato installato nella GAC, anche se cancelliamo la DLL dalla directory di lavoro, il tutto funziona sempre perché l'applicazione usa l'Assembly presente nella GAC.
Quando si installa un componente nella GAC, all'interno della GAC si crea una struttura ad albero a partire da %windir%\assembly\GAC. Per esempio, supponiamo di eseguire il seguente comando:

  gacutil /if DiegoCattaruzza.dll

All'interno della directory GAC verrà creata una directory con lo stesso nome dell'Assembly, quindi in questo caso DiegoCattaruzza (nota dell'omonimo revisore non ché coordinatore dell'Area Articoli: ogni riferimento è del tutto casuale benché voluto; non si assume alcuna responsabilità per eventuali malfunzionamenti della libreria in questione :o)). Sotto questa verrà creata un'altra directory con un nome del tipo 1.0.0.0__13b67ce9e090fefa, nome costituito dalla versione dell'Assembly (1.0.0.0) concatenata alla chiave pubblica (13b67ce9e090fefa). Questa tecnica garantisce l'univocità della directory in quanto due versioni diverse dello stesso Assembly hanno diversa posizione. All'interno della directory relativa alla versione ci saranno due files: l'Assembly DiegoCattaruzza.dll ed un file di nome __AssemblyInfo__.ini che viene generato dal comando gacutil. Quest'ultimo file ha una struttura del tipo:

  [AssemblyInfo]
  MVID=5c62b3f94bc53a4aad1df53a2d0f7e04
  URL=file:///C:/myProject/VisualBasic/bin/Debug/DiegoCattaruzza.dll
  DisplayName=DiegoCattaruzza, Version=1.0.0.0, Culture=neutral, PublicKeyToken=13b67ce9e090fefa    

Come possiamo vedere, contiene delle informazioni sull'Assembly. Ossia:

Alla luce di tutto questo, il modo migliore per disinstallare un Assembly dalla GAC, sarebbe quello di specificare tutti i riferimenti relativi allo Strong Name:

  gacutil /u DiegoCattaruzza,Version=1.0.0.0,Culture=neutral,PublicKeyToken=13b67ce9e090fefa    

invece di scrivere

  gacutil /u DiegoCattaruzza    

perché, in questo secondo caso, se ci fossero più versioni installate dello stesso componente verrebbero disinstallate tutte quante.
E' possibile, da codice, recuperare le informazioni sull'Assembly, ossia, se questo è in GAC, il percorso dove si trova e anche leggere le informazioni presenti all'interno del file __AssemblyInfo__.ini. Vediamo un frammento di codice che ci consente di fare tutto questo.

Prima di tutto all'interno del progetto devo referenziare i Namespace che mi servono:

  ' VB NET

  Imports System.Reflection
  Imports System.IO
  // C#

  using System.Reflection;
  using System.IO;

Per semplicità ho inserito la prima parte del codice all'interno della Sub di evento Load di un form:

  ' VB NET
 
  Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                         Handles MyBase.Load
    Dim executingAssembly As [Assembly] = [Assembly].GetExecutingAssembly()
    Dim strURL As String = ""
    Dim strMVID As String = ""
    Dim strDisplayName As String = ""
  
    If executingAssembly.GlobalAssemblyCache Then
      Dim myINIInfoClass As INIInfoClass = New INIInfoClass(String.Format("{0}\{1}", _
                         Path.GetDirectoryName(executingAssembly.Location), "__AssemblyInfo__.ini"))
      strURL = Path.GetDirectoryName(myINIInfoClass.getStringValue("AssemblyInfo", "URL", ""))
      strMVID = myINIInfoClass.getStringValue("AssemblyInfo", "MVID", "")
      strDisplayName = myINIInfoClass.getStringValue("AssemblyInfo", "DisplayName", "")
    Else
      strURL = Path.GetDirectoryName([Assembly].GetExecutingAssembly().CodeBase)
      strMVID = executingAssembly.FullName
    End If
  End Sub
  // C#

  private void Form1_Load(object sender, System.EventArgs e)
  {
    Assembly executingAssembly = Assembly.GetExecutingAssembly();
    string strURL = "";
    string strMVID = "";
    string strDisplayName = "";
    if(( true == executingAssembly.GlobalAssemblyCache ))
    {
      INIInfoClass myINIInfoClass = new INIInfoClass(String.Format( @"{0}\{1}",
                   Path.GetDirectoryName(executingAssembly.Location), "__AssemblyInfo__.ini" ) );
      strURL = Path.GetDirectoryName(myINIInfoClass.getStringValue("AssemblyInfo","URL", ""));
      strMVID = myINIInfoClass.getStringValue("AssemblyInfo","MVID", "");
      strDisplayName = myINIInfoClass.getStringValue("AssemblyInfo","DisplayName", "");
    }
    else
    {
      strURL = Path.GetDirectoryName(Assembly.GetExecutingAssembly().CodeBase);
      strMVID = executingAssembly.FullName;
    }
  }

Per leggere il contenuto del file INI ho costruito una semplice classe, che tra le altre cose utilizza l'API GetPrivateProfileString. La classe si chiama INIInfoClass ed è così fatta:

  ' VB NET
 
  Imports System.Text
 
  'INIInfoClass: classe per la gestione dei file INI.
  Public Class INIInfoClass
 
    'Contiene il nome del file INI completo di percorso.
    Private m_NomeFile As String
 
    'Il numero massimo di byte in una sezione.
    Private Const MAX_ENTRY = 32768
 
    'GetPrivateProfileString estrae una stringa da una specifica sezione di un file INI.
    Private Declare Ansi Function GetPrivateProfileString Lib "kernel32.dll" Alias _
                                  "GetPrivateProfileStringA" ( _
                                  ByVal lpApplicationName As String, _
                                  ByVal lpKeyName As String, _
                                  ByVal lpDefault As String, _
                                  ByVal lpReturnedString As StringBuilder, _
                                  ByVal nSize As Integer, _
                                  ByVal lpFileName As String) As Integer
 
    'Crea una nuova istanza della classe INIInfoClass.
    'file: Indica il nome del file INI completo di percorso.
    Public Sub New(ByVal file As String)
      NomeFile = file
    End Sub
 
    'Imposta o restituisce il nome del file INI completo di percorso.
    'value: Una stringa contenente il nome del file INI completo di percorso.
    Public Property NomeFile() As String
      Get
        Return m_NomeFile
      End Get
      Set(ByVal Value As String)
        m_NomeFile = Value
      End Set
    End Property
 
    'Legge una stringa da una specificata chiave della sezione specificata.
    'section: la sezione dove cercare.
    'key    : La chiave della quale restituire il valore.
    'defVal : Il valore da restituire se la chiave non viene trovata.
    'Restituisce il valore di una specificata coppia sezione/chiave, 
    '          o il valore di default se la specificata coppia sezione/chiave
    '          non viene trovata nel file INI.
    Public Function getStringValue(ByVal section As String, ByVal key As String, _
                                   ByVal defVal As String) As String
      Dim sb As StringBuilder = New StringBuilder(MAX_ENTRY)
      Dim Ret As Integer = GetPrivateProfileString(section, key, defVal, sb, MAX_ENTRY, NomeFile)
      getStringValue = sb.ToString()
    End Function
  End Class
  // C#

  using System;
  using System.Text;
  using System.Runtime.InteropServices;

  namespace MyProject
  {
    /// <summary>
    /// INIInfoClass: classe per la gesttione dei file INI.
    /// </summary>
    public class INIInfoClass
    {
      #region Variabili private e costanti
        /// <summary>
        /// COntiene il nome del file INI completo di percorso.
        /// </summary>
        private string m_NomeFile;

        /// <summary>
        /// Il numero massimo di byte in una sezione.
        /// </summary>
        private const int MAX_ENTRY = 32768;
      #endregion

      #region Dichiarazione API
        /// <summary>
        /// GetPrivateProfileString estrae una stringa da una specifica sezione di un file INI.
        /// </summary>
        [DllImport("KERNEL32.DLL", EntryPoint="GetPrivateProfileStringA", CharSet=CharSet.Ansi)]
        private static extern int GetPrivateProfileString (string lpApplicationName,
                                                           string lpKeyName, 
                                                           string lpDefault, 
                                                           StringBuilder lpReturnedString, 
                                                           int nSize,
                                                           string lpFileName);
      #endregion

      /// <summary>Crea una nuova istanza della classe INIInfoClass.</summary>
      /// <param name="file">Indica il nome del file INI completo di percorso.</param>
      public INIInfoClass(string file)
      {
        NomeFile = file;
      }

      /// <summary>Imposta o restituisce il nome del file INI completo di percorso.</summary>
      /// <value>Una stringa contenente il nome del file INI completo di percorso.</value>
      public string NomeFile
      {
        get
        {
          return m_NomeFile;
        }
        set
        {
          m_NomeFile = value;
        }
      }

      /// <summary>Legge una stringa da una specificata chiave della sezione specificata.</summary>
      /// <param name="section">La sezione dove cercare.</param>
      /// <param name="key">La chiave della quale restituire il valore.</param>
      /// <param name="defVal">Il valore da restituire se la chiave non viene trovata.</param>
      /// <returns>Restituisce il valore di una specificata coppia sezione/chiave, 
                             o il valore di default se la specificata coppia sezione/chiave 
                             non viene trovata nel file INI.</returns>
      public string getStringValue(string section, string key, string defVal)
      {
        StringBuilder sb = new StringBuilder(MAX_ENTRY);
        int Ret = GetPrivateProfileString(section, key, defVal, sb, MAX_ENTRY, NomeFile);
        return sb.ToString();
      }
    }
  }

Visualizzare i propri Assembly nella finestra di dialogo Aggiungi Riferimenti (Add Reference Dialog Box)
Come ripetuto più volte, gli Assembly che vengono registrati nella GAC sono Assembly condivisi che probabilmente verranno utilizzati da più applicazioni, quindi dopo la registrazione in GAC, ci aspetteremmo di vedere questo Assembly all'interno della finestra dei riferimenti, quando da Visual Studio .NET li vogliamo referenziare. Purtroppo non è così, il nostro Assembly non comparirà nell'elenco della finestra Aggiungi Riferimenti: per referenziarlo dovremo andare a cercarlo e referenziarlo manualmente. Il motivo principale dell'assenza del nostro Assembly dalla finestra dei riferimenti è dovuto al fatto che il contenuto di tale finestra non è basato sul contenuto della GAC e questo spiega l'assenza di alcuni Assembly. Esiste comunque un modo per far sì che tutte le volte che creo un Assembly, questo Assembly venga inserito nella finestra Aggiungi Riferimenti. Per fare questo si deve aggiungere una chiave di registro che indichi dove si trovano gli Assembly che ho creato.
Ad esempio, posto che tutti gli Assembly che creo vengano messi nella directory c:\PersonalAssemblies, devo creare, in

  HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\AssemblyFolders    

la chiave PersonalAssemblies con valore c:\PersonalAssemblies. Da notare che in questo caso, avendo creato la chiave sotto HKEY_LOCAL_MACHINE, gli Assembly saranno disponibili per tutti gli utenti che si collegheranno al sistema, se invece voglio che gli Assembly siano visibili solo per un certo utente, devo eseguire sempre la stessa operazione, ma questa volta sotto HKEY_CURRENT_USER. Per rendere effettiva la modifica e vedere così i miei Assembly nella finestra Aggiungi Riferimenti, sarà necessario riavviare Visual Studio .NET.

Registrazione di un Assembly in GAC
Come abbiamo visto fino a questo momento la registrazione di un componente in GAC può essere effettuata attraverso il tool gacutil. Oltre a questo ci sono altri metodi:

Naturalmente, come già detto, è necessario che gli Assembly abbiano tutti Strong Name altrimenti non possono essere installati in GAC.

Un add-in per la GAC
Solo per completezza, aggiungo che esiste un add-in che consente di gestire la GAC da Visual Studio .NET. Una volta installato nel menù Tools si può scegliere un nuovo strumento, l'Assembly Cache Viewer, per mezzo del quale è possibile gestire la GAC. Per scaricare l'add-in basta cliccare qui, dove troverete tutta la documentazione e le modalità di installazione e utilizzo.

Conclusioni
Spero di aver aiutato qualcuno a comprendere l'ontologia della GAC, ossia la vera natura della GAC. A parte gli scherzi ed i paroloni, spero che questo articolo possa chiarire i dubbi a coloro che si stanno avvicinando per la prima volta al mondo .NET.

Per chiarimenti o critiche o suggerimenti in merito al contenuto di questo articolo, potete scrivere all'autore, sul quale sono disponibili alcune brevi note.