Welcome back to my series on the Wix installer technologies. Previously we've looked at:

Today we're going to start dealing with a much more complex topic - adding IIS integration. To do this we'll be using the built in WiX IIS extensions, creating and calling some custom C# code and adding a new dialog screen for the options. This is where our installer starts to become really useful, and hopefully it will open your eyes to some of the possibilities of what you can automate. This article will show you the first part of this function - accessing IIS from C# code and connecting WiX to IIS. Next time we'll build on that to add a custom dialog allowing the user to select their choice of websites and app pools.

If you haven't been following the series so far, you can find the code so far here: WixInstallerExample-03.zip (731.69 kb)

We're going to start this from the wrong end logically, by writing some C# code to access IIS and read out Website and App pool information.

WiX Custom Actions

When we want to communicate with .net code from WiX we use a Custom Action assembly. This is just a normal .net assembly with all its dependencies packaged up within it and given a .CA.DLL extension. We can then access this from the WiX declarations. So now go ahead and add a new Custom Action Project called IisManager to your solution. This will open up a single file called CustomAction.cs, which will be the class WiX communicates with later. For now though you can close it down and create a new class called IisWebSite:

public class IisWebSite
{
  public IisWebSite(string id, string name)
  {
    ID = id;
    Name = name;
  }

  public string ID { get; set; }
  public string Name { get; set; }
}

This will hold the details of any web sites we find.

Next we need the logic for actually communicating with IIS. As we'll be supporting both IIS 6 and 7 we need 2 assemblies here, so import System.DirectoryServices and Microsoft.Web.Administration. Next create a new static class called IisManager and create a property called IisVersion:

public static int IisVersion
{
  get
  {
    int iisVersion;

    using (RegistryKey iisKey = Registry.LocalMachine.OpenSubKey(@"Software\Microsoft\InetStp"))
    {
      if (iisKey == null)
        throw new Exception("IIS is not installed.");

      iisVersion = (int)iisKey.GetValue("MajorVersion");
    }

    return iisVersion;
  }
}

This simply reads the version number out of the registry or warns us IIS is not installed. Next up is reading websites from IIS 6:

public static IList<IisWebSite> GetIis6Sites()
{
  List<IisWebSite> sites = new List<IisWebSite>();
  using (DirectoryEntry iisRoot = new DirectoryEntry("IIS://localhost/W3SVC"))
  {
    iisRoot.RefreshCache();

    sites.AddRange(iisRoot.Children.Cast<DirectoryEntry>().
      Where(w => w.SchemaClassName.ToLower(CultureInfo.InvariantCulture) == "iiswebserver").
      Select(w => new IisWebSite(w.Name, w.Properties["ServerComment"].Value.ToString())));
  }

  return sites;
}

This uses DirectoryEntry (the same class you'd use for accessing network user information) to identify IIS sites by the SchemaClassName and map the info to an IisWebSite object.

The IIS7 version is very similar but uses an IIS-specific set of classes:

public static IList<IisWebSite> GetIis7UpwardsSites()
{
  List<IisWebSite> sites = new List<IisWebSite>();

  using (ServerManager iisManager = new ServerManager())
  {
    sites.AddRange(iisManager.Sites.Select
      (s => new IisWebSite(s.Id.ToString(CultureInfo.InvariantCulture), s.Name)));
  }

  return sites;
}

Great, let's just add one last little method to bring it all together for ease of use later:

public static IList<IisWebSite> GetIisWebSites()
{
  return (IisVersion < 7) ? GetIis6Sites() : GetIis7UpwardsSites();
}

The app pools are similar but slightly simpler as we're just dealing with names so we can use strings instead of a custom object:

public static IList<string> GetIis6AppPools()
{
  List<string> pools = new List<string>();
  using (DirectoryEntry poolRoot = new DirectoryEntry("IIS://localhost/W3SVC/AppPools"))
  {
    poolRoot.RefreshCache();

    pools.AddRange(poolRoot.Children.Cast<DirectoryEntry>().Select(p => p.Name));
  }

  return pools;
}

public static IList<string> GetIis7UpwardsAppPools()
{
  List<string> pools = new List<string>();

  using (ServerManager iisManager = new ServerManager())
  {
    pools.AddRange(iisManager.ApplicationPools.Select(p => p.Name));
  }

  return pools;
}

public static IList<string> GetIisAppPools()
{
  return (IisVersion < 7) ? GetIis6AppPools() : GetIis7UpwardsAppPools();
}

Now is a good time to note down the details of the IIS site and app pool you'll be using. You could create a little unit test to load each and step through if you like, I created a really simple windows app which can be reused later (it's included in the code download at the end of the article):

We'll leave the custom action alone for a moment now and get back to our WiX installer.

WiX Native IIS Integrations

Here we're going to connect our installer to a specific site and app pool, so you'll need the details you noted down above. Firstly, let's get a reference to the IIS functionality. Add a reference into your installer project to WixIIsExtension.dll and WixNetFxExtension.dll from the WiX install's bin folder. This will allow us to play with websites, app pools and virtual directories. Now, in your Product.wxs add a new iis namespace to the Wix node:

<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
  xmlns:iis="http://schemas.microsoft.com/wix/IIsExtension">

What we're going to do now is add nodes representing a web site, app pool and virtual directory in IIS. After the Media node add the following:

<iis:WebSite Id="DefaultWebSite" Description="WebRoot" SiteId="1724599388">
  <iis:WebAddress Id="AllUnassigned" Port="80" />
</iis:WebSite>

<iis:WebAppPool Id="AppPool" Name="AppPool-DotNet4" />

You'll have to replace the Description, SiteId and Name attributes with your own values. This gives us a reference to the right website and app pool. It would be totally possible for you to create your own instead as part of the install process, but to do that they would have to be included inside a Component element inside the Directory node structure, which is exactly what we're about to do with the virtual directory. Add the following inside your INSTALLOCATION Directory node (remember to replace the guids with your own):

<Component Id="VirtualDirectoryComponent" Guid="GUID-HERE">
  <CreateFolder />

  <iis:WebVirtualDir Id="VirtualDirectory" Alias="WixInstallerExampleWeb"
       Directory="INSTALLLOCATION" WebSite="DefaultWebSite">
    <iis:WebApplication Id="WebSiteApp" Name="WixInstallerExampleWeb"
         WebAppPool="AppPool" />
    <iis:WebDirProperties Id="WebSite_Properties" AnonymousAccess="yes"
         WindowsAuthentication="no" DefaultDocuments="Default.aspx"
         Script="yes" Read="yes" />
  </iis:WebVirtualDir>
</Component>

<Component Id="ASPNet4Extension" Permanent="yes" Guid="GUID-HERE">
  <CreateFolder />
  <iis:WebServiceExtension Id="ASPNet4Extension" Group="ASP.NET v4.0.30319"
       Allow="yes" File="[ASPNETISAPIDLL]"
       Description="ASP.NET v4.0.30319" UIDeletable="no" />
</Component>

Pretty cool right? We've got easy access to all the settings you'd usually be able to set through the property pages on a virtual directory in IIS. Visual Studio will have full code-completion running for this so have a play and check out the settings you can use.

You'll probably have noticed the ASPNet4Extension component too - this will ensure that .net 4 is registered with IIS. It also has a property called Permanent - this ensures that when we uninstall the app that we don't remove .net4!

If you've really been following closely you'll also notice we're using a variable here - ASPNETISAPIDLL. This is the path to the .net4 isapi dll which potentially needs registering with IIS. We set this and one more variable just after the Package node at the top of the file:

<PropertyRef Id="NETFRAMEWORK40FULLINSTALLROOTDIR"/>
<SetProperty Id="ASPNETISAPIDLL" Sequence="execute" Before="ConfigureIIs"
  Value="[NETFRAMEWORK40FULLINSTALLROOTDIR]aspnet_isapi.dll" />
<SetProperty Id="ASPNETREGIIS" Sequence="execute" Before="ConfigureIIs"
  Value="[NETFRAMEWORK40FULLINSTALLROOTDIR]aspnet_regiis.exe" />

Brill, we've got a reference to not only the isapi dll, but the regiis exe. Why? Well while we've created a virtual directory and ensured we have .net4 available, we haven't actually told the virtual directory to use that version of the framework! Unfortunately this isn't just a property on the WebDirProperties node but it's still not too tricky. Add in the following right before the Feature node:

<CustomAction Id="MapVirtualDirectory"
  Directory="INSTALLLOCATION"
  Return="asyncNoWait"
  ExeCommand='[ASPNETREGIIS] -norestart -s "W3SVC/1724599388/ROOT/WixInstallerExampleWeb"' />

<InstallExecuteSequence>
  <Custom Action="MapVirtualDirectory" After="InstallFinalize"
    >ASPNETREGIIS AND NOT Installed</Custom>
</InstallExecuteSequence>

The CustomAction node makes a call to the regiis exe and passes the path to register - this includes the ID of the website and the name of the virtual directory so you'll need to change these values. The InstallExecuteSequence then tells WiX when we want the CustomAction run, and also provides a condition to make sure we have access to the regiis exe in the first place.

We're almost there now; all that remains is to add references to our new components into the Feature node to ensure they get installed:

<Feature Id="ProductFeature" Title="Wix Installer Example Web" Level="1">
  <ComponentGroupRef Id="WixInstallerExampleWeb_Project" />
  <ComponentRef Id="VirtualDirectoryComponent" />
  <ComponentRef Id="ASPNet4Extension" />
</Feature>

Checking It Works

Great, we've got a working installer! Build your app and run the installer. This time you should see "Configuring IIS" listed as one of the messages while the install progress bar builds up and right at the end a command window will appear for a second - this is the regiis exe doing it's stuff. Take a look in IIS and you should see your new site all set up and working as you would expect.

Uninstalling

Now, go to add/remove programs and remove the app you just installed. Not only will it remove the files, but it removes all references to it from IIS. Awesome huh?

I'm going to end it here even though really we haven't finished. The C# code we wrote isn't being used anywhere, and we don't want to be hard coding the website and app pool information into the installer. In the next article I'll move onto creating a custom WiX dialog for the IIS settings and connect it to the C# code we wrote earlier. If you'd like to be notified when I post this next instalment you can subscribe to my RSS feed.

See you soon!

Source Code

Here is the full source code for this example: WixInstallerExample-04.zip (1.82 mb)