Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Managed Fusion

4.96/5 (20 votes)
19 Nov 2009LGPL35 min read 50.9K   429  
Managed Fusion - A managed API to view and manipulate the Global Assembly Cache

Introduction

In this article, I'll introduce an API which provides similar functionality as gacutil, making use of Fusion.

While working on a personal project called AppStract, I ran into the need for manipulating the Global Assembly Cache (referred to as GAC) from within my code and without using an installer. While researching methods to do this, I came to the conclusion that only very little information can be found on this subject. It is very well documented what the GAC is for, while it is barely documented how to manipulate it. With this article, I intend to change this, and to provide an API to make manipulating the GAC more accessible from within the .NET Framework.

But please be warned, on MSDN, the following is documented about Fusion:

Caution: Do not use these APIs in your application to perform assembly binds or to test for the presence of assemblies or other run time, development, or design-time operations. Only administrative tools and setup programs must use these APIs. If you use the GAC, this directly exposes your application to assembly binding fragility or may cause your application to work improperly on future versions of the .NET Framework.

While what's described in the first line is basically the kind of functionality provided by this API, you should not use it lightheaded. Manipulating the GAC yourself can rarely be justified, and might introduce security risks in your application.

Requirements

Managed Fusion has the following requirements that need to be met:

  • .NET Framework version 3.5
  • The process needs sufficient rights (in most cases, administrator rights)
  • Only strong signed assemblies can be installed to the GAC

Using the Code

The API resides in the System.Reflection.GAC namespace, and exposes the following types and their public methods:

Managed fusion class diagram

  • AssemblyCache - This is the main entry type for the API. When constructing an instance of this type, an InstallerDescription is required.
  • InstallerDescription - This class enables the API to make use of reference counting when manipulating the GAC, in the same way as MSI uses reference counting. This type can also be seen as the managed counterpart of FUSION_INSTALL_REFERENCE.
  • InstallerType - A property of InstallerDescription, defining the type of the installing application.
  • InstallBehaviour - Defines the three possible rules on how an assembly has to be installed in the GAC.
  • UninstallDisposition - Represents the result of an attempt to uninstall an assembly from the Global Assembly Cache.

As already said, AssemblyCache is the main type of this API; it exposes all possible ways of interacting with the GAC. It implements the IEnumerable<AssemblyName> interface, and exposes four public methods:

  • AssemblyCache.InstallAssembly(AssemblyName, InstallBehaviour)
  • AssemblyCache.UninstallAssembly(AssemblyName)
  • AssemblyCache.IsInstalled(AssemblyName)
  • AssemblyCache.GetReferences(AssemblyName)

The names of these methods should speak for themselves. Although for InstallAssembly, an important side note needs to be made, the AssemblyName parameter needs to specify a valid value for its CodeBase property; in essence, this means that the property needs to point to a strong signed assembly which is persisted in the file system.

For InstallerDescription, the API defines some public properties, and three static methods instead of a public constructor:

  • InstallerDescription.CreateForInstaller() - Creates a describer for an installer; this installer should always be an application that appears in the list of Add/Remove Programs.
  • InstallerDescription.CreateForFile() - Creates a describer for an application that is represented by a file in the file system.
  • InstallerDescription.CreateForOpaqueString() - Creates a describer for an application that is only represented by an opaque string.

Using one of these methods is the only way to get an instance of InstallerDescription. All methods take two strings as parameters: the first one is basically a description, while the second string is used as an identifier for the application. As you may have noticed, the InstallerType enum defines five values, while only three types are used by the methods described above. This is because the values WindowsInstaller and OperatingSystem are reserved by MSI and the Windows Operating System, and should therefore never be used by any other application.

Viewing the GAC

AssemblyCache enables you to enumerate all assemblies installed in the Global Assembly Cache, to enumerate all references on an assembly, and to verify if an assembly is installed in the GAC. All of this can be done without the need for administrator privileges.

The following is a basic example on enumerating the GAC and enumerating all references on installed assemblies, while printing the results to the console window:

C#
using System;
using System.Reflection.GAC;

namespace FusionTestApplication
{
  class Program
  {
    private const string _rndm = "someRandomString";

    static void Main()
    {
      Console.WriteLine("This program will enumerate all " + 
                        "assemblies in the GAC, and their references.");
      Console.WriteLine("For displaying purposes it is recommended " + 
                        "to widen your console window.");
      Console.WriteLine("Press enter to continue...");
      Console.ReadLine();
      InstallerDescription description = 
        InstallerDescription.CreateForOpaqueString("Enumerating the GAC", _rndm);
      AssemblyCache cache = new AssemblyCache(description);
      // Enumerate all assemblies in the GAC.
      foreach (var assembly in cache)
      {
        Console.WriteLine(assembly.FullName);
        foreach (var rfr in cache.GetReferences(assembly))
          Console.WriteLine("\t" + rfr);
      }
      Console.ReadLine();
    }
  }
}

And here is an example of how to verify if an assembly is installed to the GAC:

C#
using System;
using System.Reflection.GAC;

namespace FusionTestApplication
{
  class Program
  {
    private const string _rndm = "someRandomString";

    static void Main()
    {
      InstallerDescription description = 
        InstallerDescription.CreateForOpaqueString(
        "Verifying the existence of assemblies", _rndm);
      AssemblyCache cache = new AssemblyCache(description);
      while (true)
      {
        Console.WriteLine("Specify an assembly name:");
        string value = Console.ReadLine();
        bool isInstalled = cache.IsInstalled(new AssemblyName(value));
        Console.WriteLine("IsInstalled: " + isInstalled + Environment.NewLine);
      }
    }
  }
}

Manipulating the GAC

Please remember, for manipulating the GAC, you always need administrator privileges.

The following is an example of how you can install an assembly called "someTestAssembly.dll" to the Global Assembly Cache:

C#
// First create an InstallerDescription. For this example
// we'll use the executable of the current process as a reference
InstallerDescription installer = InstallerDescription.CreateForFile(
     "Test application for Managed Fusion", 
     Process.GetCurrentProcess().MainModule.FileName);
// Then we can create an AssemblyCache
AssemblyCache gac = new AssemblyCache(installer);
// Now load the assembly, verify it's strong named,
// and get its full AssemblyName
Assembly assembly = Assembly.ReflectionOnlyLoadFrom(file.FileName);
if (assembly.GetName().GetPublicKey().Length != 0)
{
  // Calling Assembly.GetName() automatically assigns AssemblyName.CodeBase
  AssemblyName assemblyName = assembly.GetName();
  gac.InstallAssembly(assembly, InstallBehaviour.Default);
}

The following is an example of how you can uninstall the assembly called "someTestAssembly.dll" from the Global Assembly Cache:

C#
// Create the same InstallerDescription
// as the one used when installing the assembly.
InstallerDescription installer = InstallerDescription.CreateForFile(
   "Test application for Managed Fusion", 
   Process.GetCurrentProcess().MainModule.FileName);
// Initialize an AssemblyCache
AssemblyCache gac = new AssemblyCache(installer);
// Construct an AssemblyName. Remember: the more specific the better.
AssemblyName assemblyName = new AssemblyName("someTestAssembly.dll, Version=1.0.0.0");
UninstallDisposition result = gac.UninstallAssembly(assembly);

Points of Interest

Fusion and Assembly Names

For some reason, Fusion won't accept a fully specified assembly name such as:

myAssembly, Version=2.0.0.0, Culture=neutral, PublicKeyToken=0038abc9deabfle5

After some testing, it seems like the issue is caused by the PublicKeyToken attribute. As a preliminary workaround for this issue, an extension method has been added to AssemblyName which returns a fully specified assembly name excluding PublicKeyToken.

C#
public static string GetFusionCompatibleFullName(this AssemblyName assemblyName)
{
  return assemblyName.Name
         + (assemblyName.Version == null? "" : ", Version=" + assemblyName.Version)
         + (assemblyName.CultureInfo == null || 
            string.IsNullOrEmpty(assemblyName.CultureInfo.Name) ? "" : ", 
            Culture=" + assemblyName.CultureInfo.Name);
}

Marshal.SizeOf()

An issue that cost me quite a lot of time is Marshal.SizeOf() returning an incorrect value for the FusionInstallReference struct. When this method is called from within the struct, it returns a size of 32 bytes. This size is needed to set the FusionInstallReference._size variable, which is a required variable for marshalling the struct to the Fusion API. After some testing, I concluded that when the Marshal.SizeOf() is called from another class or struct, it returns the correct value, which is 40 bytes. This value is now hardcoded in the constructor of FusionInstallReference.

Call for Help

Please report any issues you run into (bugs, undocumented exceptions, ...), and any possible enhancements or solutions you might find.

References

History

  • 14/11/2009 - Initial release
  • 17/11/2009 - Updated source code

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)