Registering an Assembly for COM Interop in a MSI file with the Windows Installer XML toolset.

On an internal mailing list, Omar Shahine just posted a link to his blog entry about how to register an assembly for COM Interop via an MSI.  After getting about halfway through the entry I started getting concerned.  Omar was suggesting that your assembly should be registered by calling into a custom System.Configuration.Install.Installer class via a CustomAction.  I firmly believe that CustomActions should be used only as a last resort, but a full body shiver hit me when I read the part where Omar said:

Now you can safely rely on MSI to register your component for COM Interop.

This statement is false.  While Omar does mention that you need to create CustomActions for both install and uninstall, he has missed the ever elusive but extremely important rollback CustomAction.  Rollback CustomActions are important to ensure that if anything goes wrong during the installation (or uninstallation) of your product that each of your custom processing gets appropriately cleaned up.  Rollback CustomActions are vital to ensure you end up with a proper compensating transaction for your install.

Currently, Omar's suggested solution for registering an assembly for COM Interop has the potential to leave the registration for your assembly on the machine even though an error occurred during installation and MSI removed the assembly itself from the machine.  That scenario is obviously undesirable.  You've not only dirtied the user's machine with invalid configuration, other applications may attempt to activate your assembly via COM and fail in mysterious ways.  An even more insidious scenario is the fact that it is possible for the uninstall to fail and rollback in such a way that the assembly and product remain on the machine but the COM Interop registration has been removed.  I would not want to be the support engineer trying to debug that problem.

Now a lot of people like to argue with me that failures during install don't happen in "the wrong places" all that often and that failures during uninstall "hardly ever happen".  Well, I cut my teeth doing deployment in Office and when you have hundreds of thousands of users you find that installation and uninstallation will fail in every which way it can.  But, I don't usually bring that up first.  Instead, I usually point out that a user canceling your setup is essentially the same as rollback.  Sometimes they try to argue that they will just hide the Cancel button, but then I point how lousy a user experience that is.  Imagine not being able to go, "Oops, I didn't mean to install/uninstall that!"  When I get someone who doesn't care about the user experience (I mentally note to send a message to their manager <grin/>) and point out how it is impossible to get the Windows Installer to completely prevent the user from canceling setup in all cases.

Okay, so hopefully you now agree there is a good reason to have a rollback CustomAction.  I bet someone well versed in Visual Studio deployment projects could build on Omar's blog to add the necessary rollback CustomActions (yes, plural). So let me bring out the real problem (I know, I know, you probably thought I was done).  The installation or uninstallation of the registration is not tied to a Component.

"So what?" you might ask.  Well, let's imagine a scenario where your assembly is reusable and you want to use it in multiple products.  In other words, the assembly file and registration CustomActions show up in multiple MSI files.  Now let's say someone thinks you write really cool stuff and installs a couple of your products on their machine.  Interestingly enough, the registration for your assembly's COM Interop is written twice (once for each install), but that's no big deal.  Then let's say the user decides one of your products isn't as useful as advertised and removes the product.  When the registration CustomAction executes it will remove all the registration for your assembly's COM Interop.  This wouldn't be a problem except that you still have a product on the machine that needs the COM Interop.

There are lots of different ways to hit the problems described in the scenario above that you wouldn't necessarily think of right away (unless you've been twisted by years of fighting with lazy developers who think "setup is just xcopy" <grin/>).  Anyway, the key point to remember is that the Windows Installer controls the installation and uninstallation of Resources (like the assembly file and each of its COM Interop registry keys) via Components.  If you want more reading, I have an old blog entry about the topic.

So now you might be thinking, "Thanks, Rob.  You've ruined my day."  Well, there are a few things that we can do.  First, you could track down all of the registry keys that the CustomAction would have written and just author them in the Component.  I think this is the part that Omar said frustrated him the most, so that might not be really enticing even if it a "safer solution".  Second, you could try to condition the execution of all of your registration CustomActions based on the install state of the Component that contains the assembly being registered.  This option can be very error prone (getting the condition just right is tricky) and difficult to reuse.  Which brings me to my final option, use the Windows Installer XML toolset to automatically populate your MSI (or MSM) with the appropriate COM Interop registration.

Using the WiX toolset to populate your MSI is like getting the first option but without all the work of finding the registry keys.  The authoring to do something like this would be, notice the use of the "AssemblyRegisterComInterop" attribute:

 <?xml version='1.0'?> 
<Wix xmlns='https://schemas.microsoft.com/wix/2003/01/wi'> 
   <Product Id='G-U-I-D-Here' Name='TestAssemblyProduct' Language='1033' Version='0.0.0.0' Manufacturer='Your name goes here'> 
      <Package Description='Test Assembly in a Product' Comments='Demonstrate Assembly COM Interop Regsitration' InstallerVersion='200' Compressed='yes' /> 
      <Media Id='1' Cabinet='product.cab' EmbedCab='yes' /> 
      <Directory Id='TARGETDIR' Name='SourceDir'> 
         <Directory Id='ProgramFilesFolder' Name='PFiles'> 
            <Directory Id='TestAssemblyProductDirectory' Name='testassm' LongName='Test Assembly'> 
               <Component Id='TestAssemblyProductComponent' Guid='G-U-I-D-Here'> 
                  <File Id='TestAssemblyProductFile' Name='assembly.dll' Assembly='.net' AssemblyRegisterComInterop='yes' KeyPath='yes' DiskId='1' /> 
               </Component> 
            </Directory> 
         </Directory> 
      </Directory> 
      <Feature Id='TestAssemblyProductFeature' Title='Test "ssembly Product Feature' Level='1'> 
         <ComponentRef Id='TestAssemblyProductComponent' /> 
      </Feature> 
   </Product>
</Wix> 

(note: I did not try compiling/linking this, but it should definitely point you in the right direction).

Since this blog entry turned out pretty long, let me see if I can sum up the points:

  1. CustomActions should be avoided unless there is absolutely no other means to accomplish the task.
  2. CustomActions that modify the user's machine should always have associated rollback CustomActions.
  3. CustomActions that install/uninstall Resources should always have their execution tied to a Component's install state.
  4. Being lazy when creating your setup package will create grief for your customers and your product support team.
  5. Setup isn't just xcopy.
  6. The WiX toolset has a helper attribute on the File element for registering COM Interop for assemblies.