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:
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 string
s 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:
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);
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:
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:
InstallerDescription installer = InstallerDescription.CreateForFile(
"Test application for Managed Fusion",
Process.GetCurrentProcess().MainModule.FileName);
AssemblyCache gac = new AssemblyCache(installer);
Assembly assembly = Assembly.ReflectionOnlyLoadFrom(file.FileName);
if (assembly.GetName().GetPublicKey().Length != 0)
{
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:
InstallerDescription installer = InstallerDescription.CreateForFile(
"Test application for Managed Fusion",
Process.GetCurrentProcess().MainModule.FileName);
AssemblyCache gac = new AssemblyCache(installer);
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
.
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