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.

Using the Code

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

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:

Manipulating the GAC

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

// 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:

// 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:

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.

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.

One of the many purposes of namespaces is to provide a fundamental unit of logical code grouping.
And since System.Reflection is already intended to group everything you need to interact with assemblies, I found it a logic choice to add the AssemblyCache type to the same namespace.

Please, he is right. Even Microsoft does not use the System.* namespace, only the .NET framework team.

Simon Allaeys wrote:

And since System.Reflection is already intended to group everything you need to interact with assemblies

So, if anyone writes a new Window Forms control, ifit must be placed in the System.Windows.Forms namespace?

Simon Allaeys wrote:

I found it a logic choice to add the AssemblyCache type to the same namespace.

If the .NET team decided to add a similarly named class, then one has to rewrite the application?
Beside preventing use of similar names from several vendors, namespace is useless, so if no attempt is made to prevent even this, then what is the value?

But, personally, I will not care much since I will rename the namespace if I need to use it.