Some platform features are like the stuff hiding under rocks at the seashore. No, I don't mean the one's that are slimey and stinky. I'm talking about the one's that are interesting but hard to spot. But, all you need to do is catch a glimpse of them and lift the rock...

Sometime last year, I was reading a page in the MSDN documentation when a certain line caught my attention. The page was "Locating the Assembly through Codebases or Probing" and the line was this one, at the very bottom of the page:

"If Assembly2 is not found at either of those locations, the runtime queries the Windows Installer."

For some reason, this line struck me as interesting – a hitherto unseen mystery in the .NET runtime. OK, it's not like I was finding some hidden clue in the Hypernotomachia Poliphili or anything. But this is my world, and I take my kicks where I can get 'em.

I'd read this line many times before, but I'd never really thought anything of it. Now I was suddenly realizing in all the times I'd seen assembly probing fail, I'd never ever seen it report that it was asking the Windows Installer to find the assembly. Was this just a documentation error, or was this a real feature of the .NET runtime?

To test things out, I created a simple application. It basically consisted of a managed executable (A) that loaded a library (B) using Assembly.Load(AssemblyName). Trying to do things the fastest way possible, I created a Windows installer by creating a Setup Project in VS 2003. I installed the application, deleted library B and ran A, and sure enough there was an assembly load exception, but there was no message in the Fusion log about the Windows Installer being invoked to find the assembly.

I sent of a couple of e-mails to Microsoft folks, and got a response back from the .NET team that indicated that if all was well with the installation, a call to MsiProvideAssembly with the correct parameters should cause the missing assembly to reappear. OK, so .NET must be relying on this call internally to find the assembly, right? I ran the installed application under a debugger, and I did not see msi.dll, the Windows Installer library, being loaded by the .NET runtime. Very strange. A quick peek confirmed that any Windows Installer probing code had been stripped out of the Rotor sources, so no help there.

So I logged a bug at the Feedback center. It even got a mention in Junfeng Zhangs blog, one of the CLR loader wizards. The problem, it would appear, was with the installer built by the VS 2003 Setup project. It was not doing the right magic when it installed the application to allow .NET to find the assembly. What that magic was, and why .NET never loaded msi.dll remained a mystery.Well, I did eventually figure out what was going on, but I never got around to blogging about it.

So for the other pathologically curious folks out there, here's the answer to the mystery. Firstly, as we've already established, .NET is not really at fault. It is however up to some of its usual sneakiness in the never ending quest for performance, which is what made it hard for me to point the finger at the actual cause of the problem in the first place. The fact is, the .NET runtime doesn't even bother loading msi.dll unless it can be certain that the cost will be worthwhile. In order to do that, it actually does some of the checks that MsiProvideAssembly would do. That's why I never saw msi.dll being loaded. To discover this I ran Regmon on the installed application (with the library deleted), on the assumption that that was the most likely place for further clues, and searched for the word "Installer" in the output. It turns out that the runtime scans the following list of registry keys for a record of the missing assembly:

HKLM\SOFTWARE\Microsoft\Windows CurrentVersion\Installer\Managed\<SID>  Installer\Assemblies\Global\<Strong Name>
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer Managed\<SID>\Installer\Assemblies\<File Path>HKCU\Software\Microsoft\Installer\Assemblies Global\<Strong Name>
HKCU\Software\Microsoft\Installer\Assemblies\<File Path>HKLM\SOFTWARE\Classes\Installer\Assemblies\Global\<Strong Name>
HKLM\SOFTWARE\Classes\Installer\Assemblies\<File Path>\

If any record of the assembly being installed is found, the runtime loads msi.dll to bring back the missing assembly. <SID> appears to be the security identifier of the user that the process is running as. <File Path> is the full path to the file, but any '\' is substituted with a ''. <Strong Name> is the full assembly strong name. "Global" refers to the Global Assembly Cache (GAC).

Unfortunately, in the case that the assembly is found the Windows Installer obviously scans the same registry keys again looking for the assembly. I won't hide the fact that I don't like these sorts of shenanigans – by making assumptions about the behavior of the Windows Installer, the Windows Installer team is now essentially prevented from making changes to the way that MsiProvideAssemby works without a corresponding update to the .NET runtime. But what's done is done.

So how do you create an installer that will cause assembly probing to query the Windows Installer and bring back missing assemblies? Well, I'm sure that one of the commercial installers will do the right thing, but my preferred solution is just to use WiX, given that it's free.

So, let's build another sample from scratch. Firstly, we'll create an assembly called Binding.exe. Here's the code:

using System;
using System.Reflection;
using System.Globalization;
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyKeyFile("binding.snk")]
namespace Binding
{
 class MainClass
 {
  static void Main(string[] args)
  {
   AssemblyName myAssemblyName = new AssemblyName();
   myAssemblyName.Name = "ClassLibrary1";
   myAssemblyName.Version = new Version("1.0.0.0");
   myAssemblyName.SetPublicKeyToken(new byte[]
    {0x32, 0xea, 0x68, 0xe6, 0x16, 0x01, 0x2a, 0xe3});
   myAssemblyName.CultureInfo = new CultureInfo("");
   Assembly ass = Assembly.Load(myAssemblyName);
   MethodInfo method =
    ass.GetType("ClassLibrary1.Class1").GetMethod("Method1");
   method.Invoke(null, null);
  }
 }
}

Just to make things interesting this code builds an AssemblyName and loads an assembly explicitly. Note that everything should be strongly named in order for things to work properly. Don't forget to set the CultureInfo. The code for ClassLibrary1.dll is just:

using System;
using System.Reflection;
using ClassLibrary2;

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyKeyFile("binding.snk")]

namespace ClassLibrary1
{
  public class Class1
  {
    public Class1()
    {
    }
   
    public static void Method1()
    {
      Console.WriteLine("ClassLibrary1.Class1.Method1");
      Class2.Method2();
    }
  }
}

ClassLibrary1 has an implicit dependency on the assembly ClassLibrary2.dll. The code for ClassLibrary2.dll is:

using System;
using System.Reflection;
using ClassLibrary2;[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyKeyFile("binding.snk")]namespace ClassLibrary1
{
  public class Class1
  {
    public Class1()
    {
    }
    
    public static void Method1()
    {
      Console.WriteLine("ClassLibrary1.Class1.Method1");
      Class2.Method2();
    }
  }
}

OK, now for the installer code:

<?xml version="1.0" encoding="utf-8"?>
  <Wix xmlns=http://schemas.microsoft.com/wix/2003/01/wi>
  <Product Id="DEB8151D-1EEB-4549-A5AD-405C4CD28085" Name="BindingProduct" Language="1033" Version="0.0.0.0" Manufacturer="Microsoft Corporation">
  <Package Id="511F5323-27B5-47C1-A488-8F92ED38EFEB" Description="Binding Test Product" Comments="Used for testing assembly binding" InstallerVersion="200" Compressed="yes" />
  <Media Id="1" Cabinet="PRODUCT.CAB" EmbedCab="yes" />
  <Directory Id="TARGETDIR" Name="SourceDir">
    <Directory Id="ProgramFilesFolder" Name="PFiles">
      <Directory Id="BindingProductDirectory" Name="ProdDir" LongName="BindingTest">
        <Component Id="BindingComponent" Guid="3A505CEA-C035-4F4C-A7CD-C0CAE01F492F">
          <File Id="BindingExeConfigFile" Name="BINDIN01.CON" LongName="Binding.exe.config" DiskId="1" src="Binding.exe.config" mce_src="Binding.exe.config"/>
          <File Id="BindingExeFile" Name="BINDING.EXE" LongName="Binding.exe" DiskId="1" src="Binding.exe" mce_src="Binding.exe" Assembly=".net" KeyPath="yes" AssemblyApplication="BindingExeConfigFile" AssemblyManifest="BindingExeFile" Vital="yes"/>
        </Component>
        <Component Id="ClassLibrary1Component" Guid="F6E5ECF8-EBD8-42C1-8BC0-9A3E7CAF985C">
          <File Id="ClassLibrary1DllFile" Name="CLASSL01.DLL" LongName="ClassLibrary1.dll" DiskId="1" src="ClassLibrary1.dll" mce_src="ClassLibrary1.dll" Assembly=".net" KeyPath="yes" AssemblyApplication="BindingExeConfigFile" AssemblyManifest="BindingExeFile" Vital="yes"/>
        </Component>
        <Component Id="ClassLibrary2Component" Guid="FCEAC462-3710-4A01-ACAB-B41F522ACD41">
          <File Id="ClassLibrary2DllFile" Name="CLASSL02.DLL" LongName="ClassLibrary2.dll" DiskId="1" src="ClassLibrary2.dll" mce_src="ClassLibrary2.dll" Assembly=".net" KeyPath="yes" AssemblyApplication="BindingExeConfigFile" AssemblyManifest="BindingExeFile" Vital="yes"/>
        </Component>
      </Directory>
    </Directory>
  </Directory>
  <Feature Id="BindingProductFeature" Title="Binding Test Product Feature" Level="1">
    <ComponentRef Id="BindingComponent" />
    <ComponentRef Id="ClassLibrary1Component"/>
    <ComponentRef Id="ClassLibrary2Component"/>
  </Feature>
  </Product>
</Wix>

The key component of the installer is the use of the AssemblyApplication attribute on the File element. This attribute points to the Binding.exe.config file for the application. This is the file that .NET will look for to determine if your application is installed before calling the Windows Installer bring back any missing assemblies.

Compile the three assemblies, then the installer. Run the installer. No navigate to the folder C:\Program Files\BindingTest\. Run Binding.exe. It outputs the two method names. Now for the magic. Delete ClassLibrary1.dll and ClassLibrary2.dll. Go on, don't be shy. Done that? Now run Binding.exe again. Instead of an AssemblyLoadException the program should run as if nothing had happened. Check again and the two DLL's are magically back. A self repairing application. Pretty cool, eh?

Lastly, I built a small program to test things out called CheckInstalled.cs, as follows:

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Reflection;
using System.Text;

// After running msiexec /i WixSetup.msi the command:
//
// CheckInstalled ClassLibrary1 "C:\Program Files\BindingTest\Binding.exe"
//
// Should return the path to ClassLibrary1.dll. See one of the registry keys:
//
// HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\Managed\
//   <SID>\Installer\Assemblies\Global\<Strong Name>
// HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\Managed\
//   <SID>\Installer\Assemblies\<File Path>
// HKCU\Software\Microsoft\Installer\Assemblies\Global\<Strong Name>
// HKCU\Software\Microsoft\Installer\Assemblies\<File Path>
// HKLM\SOFTWARE\Classes\Installer\Assemblies\Global\<Strong Name>
// HKLM\SOFTWARE\Classes\Installer\Assemblies\<File Path>

class MainClass
{
  [DllImport("msi.dll", CharSet=CharSet.Auto, SetLastError=true)]
  static extern uint MsiProvideAssembly(
    string assembly, string context, int mode, int info,
    System.Text.StringBuilder path, ref int pathSize); 
 
  public static void Main(String[] args)
  {
    if (args.Length < 1)
    {
      Console.WriteLine(
        "USAGE: {0} <assemblyName> [<installPath>]",
        Path.GetFileName(Assembly.GetExecutingAssembly().Location));
      Console.WriteLine(
        "\nAssembly name is just a full or partial assembly name. " +
        "The <installPath> is case sensitive");
      return;
    }
    
    if (args.Length > 1)
      Console.WriteLine(
        "Checking for installation of assembly '{0}' at install path '{1}'",
        args[0], args[1]);
    else
      Console.WriteLine("Checking for installation of assembly '{0}'",
        args[0]);
  
    try
    {
      Console.WriteLine(EnsureAssemblyIsInstalled(
        args[0], args.Length < 2 ? null : args[1]));
    }
    catch (SystemException e)
    {
      Console.WriteLine("ERROR: " + e.Message);
    }
  }
 
  public static string EnsureAssemblyIsInstalled(
    string assemblyName, string installPath)
  {
    int buffer = 1024;
    StringBuilder path = new StringBuilder(1024);  
    uint code = MsiProvideAssembly(assemblyName, installPath,
      0, 0, path, ref buffer);    
    if (code != 0)
      throw new SystemException("Windows Installer error " +
        code.ToString());   
      return "Found '" + path.ToString() + "'";
  }
}

Run this program as follows:

CheckInstalled ClassLibrary1 "C:\Program Files\BindingTest\Binding.exe.config"

It should report that ClassLibrary1 was found. Delete it and run the application again and you'll see Windows Installer bring the file back, this time with a user interface. This UI is suppressed by the .NET runtime.

So next time you see something hiding underneath a rock, go and check it out. It might be nothingit could be a nasty smelly bug, or it could just be a hidden gem.