Creating a Setup Project for IIS Extensions using Visual Studio 2008

IntroductionIIS 7 provides a rich extensibility model, whether extending the server or the user interface, one critical thing is provide a simple setup application that can install all the required files, add any registration information required, and modify the server settings as required by the extension.
Visual Studio 2008 provides a set of project types called Setup and Deployment projects specifically for this kind of applications. The output generated for these projects is an MSI that can perform several actions for you, including copying files, adding files to the GAC, adding registry keys, and many more.
In this document we will create a setup project to install a hypothetical runtime Server Module that also includes a User Interface extension for IIS Manager.
Our setup will basically perform the following actions:
•    Copy the required files, including three DLL’s and an html page.
•    Add a couple of registry keys.
•    Add the managed assemblies to the GAC
•    Modify applicationHost.config to register a new module
•    Modify administration.config to register a new UI extensibility for InetMgr
•    Create a new sample application that exposes the html pages
•    Finally, we will remove the changes from both configuration files during uninstall

Creating the Setup Project

Start Visual Studio 2008. In the File Menu, select the option New Project.
In the New Project Dialog, expand the Other Project Types option in the Project type tree view.
Select the option Setup and Deployment type and select the option Setup Project. Enter a name for the Project and a location. I will use SampleSetup as the name.

image

Adding Files to the Setup

  • Select the menu View->Editor->File System. This will open the editor where you can add all the files that you need to deploy with your application. In this case I will just add an html file that I have created called readme.htm.
  • To do that, right click the Application Folder directory in the tree view and select the option Add File. Browse to your files and select all the files you want to copy to the setup folder (by default <Program Files>\<Project Name> .

Adding assemblies to the GAC

Adding assemblies to the setup is done in the same File System editor, however it includes a special folder called Global Assembly Cache that represents the GAC in the target system.
In our sample we will add to the GAC the assemblies that have the runtime server module and the user interface modules for IIS Manager. I have created the following set of projects:

  1. SampleModule.dll that includes the runtime module on it.
  2. SampleModuleUI.dll that contains the server-side portion of the IIS Manager extension (ModuleProvider, ModuleService, etc).
  3. SampleModuleUIClient.dll that contains the client side portion of the IIS Manager extension (Module, ModulePage, TaskLists, etc).


Back in Visual Studio,

  • Select the menu option View->Editor->File System
  • Right-click the root node in the Tree view titled File System on Target Machine and select the option Add Special Folder.
    Select the option Global Assembly Cache Folder.
    Right click the newly added GAC folder and choose the option Add File and browse to the DLL and choose OK. Another option is using the Add Assembly and use the "Select Component" dialog to add it.
    Visual Studio will recognize the dependencies that the assembly has, and will try to add them to the project automatically. However, certain assemblies such as Microsoft.Web.Administration, or any other System assemblies should be excluded because they will already be installed in the target machine.
  • To ensure that you don't ship system assemblies, in the Solution Explorer expand the Detected Dependencies folder and right click each of the assemblies that shouldn't be packaged and select the option Exclude. (In our case we will exclude Microsoft.Web.Administration.dll, Microsoft.Web.Management.dll, Microsoft.ManagementConsole.dll and MMCFxCommon.dll)
    After completing this, the project should look as follows:

image

Adding Registry Keys

Visual Studio also includes a Registry editor that helps you adding any registry keys in the target machine. For this sample I will just add a registry key in HKEY_LOCAL_MACHINE\Software\My Company\Message. For that:
Select the menu option View->Editor->Registry.
Expand the HKEY_LOCAL_MACHINE node and drill down to Software\[Manufacturer].
[Manufacturer] is a variable that holds the name of the company, and can be set by selecting the SampleSetup node in Solution Explorer and using the Property Grid to change it. There are several other variables defined such as Author, Description, ProductName, Title and Version that helps whenever dynamic text is required.
Right click [Manufacturer] and select the option new String Value. Enter Message as the name. To set the value you can select the item in the List View and use the Property Grid to set its value.
After completing this, the project should look as follows:

clip_image002

Executing Custom Code

To support any custom code to be executed when running the setup application, Visual Studio (more explicitly MSI) supports the concept of Custom Actions. These Custom Actions include running an application, a script or executing code from a managed assembly.
For our sample, we will create a new project where we will add all the code  to read and change configuration.
Select the option File->Add->New Project.
Select the Class Library template and name it SetupHelper.

image

  • Since we will be creating a custom action, we need to add a reference to System.Configuration.Install to be able to create the custom action. Use the Project->Add Reference. And in the .NET Tab select the System.Configuration.Install and press OK.
  • Since we will also be modifying server configuration (for registering the HTTP Module in ApplicationHost.config and the ModuleProvider in administration.config) using Microsoft.Web.Administration we need to add a reference to it as well. Again use the Project->Add Reference, and browse to <windows>\system32\inetsrv and select Microsoft.Web.Administration.dll
  • Rename the file Class1.cs file to be named SetupAction.cs and make the class name SetupAction. This class needs to inherit from System.Configuration.Install.Installer which is the base class for all custom actions and it has several methods that you can override to add custom logic to the setup process. In this case we will add our code in the Install and the Uninstall method.

using System;
using System.ComponentModel;
using System.Configuration.Install;

namespace SetupHelper {
    [RunInstaller(true)]
    public class SetupAction : Installer {
        public override void Install(System.Collections.IDictionary stateSaver) {
            base.Install(stateSaver);

            InstallUtil.AddUIModuleProvider(
"SampleUIModule",
"SampleUIModule.SampleModuleProvider, SampleUIModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=12606126ca8290d1"
            );

            // Add a Server Module to applicationHost.config
            InstallUtil.AddModule(
"SampleModule",
"SampleModule.SampleModule, SampleModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=12606126ca8290d1"
            );

            // Create a web application
            InstallUtil.CreateApplication(
"Default Web Site",
"/SampleApp",
                Context.Parameters["TargetDir"]
            );
        }

        public override void Uninstall(System.Collections.IDictionary savedState) {
            base.Uninstall(savedState);

            InstallUtil.RemoveUIModuleProvider("SampleUIModule");
            InstallUtil.RemoveModule("SampleModule");
            InstallUtil.RemoveApplication("Default Web Site", "/SampleApp");
        }
    }
}
   

As you can see the code above is actually really simple, it just calls helper methods in a utility class called InstallUtil that is shown at the end of this entry. You will also need to add the InstallUtil class to the project to be able to compile it. The only interesting piece of code above is how we pass the TargetDir from the Setup project to the Custom action through the Parameters property of the InstallContext type.

Configuring the Custom Action

To be able to use our new Custom Action we need to add the SetupHelper output to our setup project, for that:
Select the option View->Editor->File System
Right-click the Application Folder node and select the option Add Project Output... and select the SetupHelper project in the Project drop down.

image

After doing this, the DLL will be included as part of our setup.

Adding the Install Custom Action

Select the option View->Editor->Custom Actions
Right-click the Install node and select the option Add Custom Action… drill down into the Application Folder and select the Primary output from SetupHelper.

image

Click OK and type a name such as InstallModules

Now, since we want to pass the TargetDir variable to be used as the physical path for the web application that we will create within our Installer derived-class, select the custom action and go to the Property Grid. There is a property called CustomActionData. This property is used to pass any data to the installer parameters class, and uses the format “/<name>=<value>”. So for our example we will set it to: /TargetDir="[TARGETDIR]\"

image

Adding the Uninstall Custom Action

In the same editor, right-click the Uninstall node and select the option Add Custom Action…, again drill down into the Application Folder and select the Primary output from SetupHelper.
Press OK and type a name such as UninstallModules.
After doing this the editor should look as follows:

image

Building and Testing the Setup

Finally we can build the solution by using the Build->Rebuild Solution menu option.
This will create a file called SampleSetup.msi, in the folder SampleSetup\SampleSetup\Debug\SampleSetup.msi
You can now run this MSI and it will walk through the process of installing. The user interface that is provided by default can also be configured to add new steps or remove the current steps. You can also provide a Banner logo for the windows and many more options from the View->Editor->User Interface.

clip_image002[4]clip_image002[6]

Visual Studio provides different packaging mechanisms for the setup application. You can change it through the Project Properties dialog where you get the option to use:
1)    As loose uncompressed files. This option packages all the files by just copying them into a file system structure where the files are copied unchanged. This is a good packaging option for CD or DVD based setups
2)    In setup file. This option packages all the files within the MSI file
3)    In cabinet files. This option creates a set of CAB files that can be used in scenarios such as diskette based setup.

You can also customize all the setup properties using the property grid, such as DetectNewerInstalledVersion which will warn users if a newer version is already installed or RemovePreviousVersion that will automatically remove older versions for the user whenever he tries to install a new one.

 

64-bit considerations

Turns out that the managed code custom action will fail under 64-bit platform due to it being executed as a 32-bit custom action the following blog talks about the details and shows how you can fix the issue:

https://blogs.msdn.com/heaths/archive/2006/02/01/64-bit-managed-custom-actions-with-visual-studio.aspx

 

 

Summary

Visual Studio 2008 provides a simple option to easily create Setup applications that can perform custom code through Custom actions. In this document we created a simple custom action to install modules and InetMgr extensions through this support.

For the latest information about IIS 7.0, see the IIS 7 Web site at https://www.iis.net

InstallUtil

This is the class that is used from the SetupHelper class we created to do the actual changes in configuration. As you can see it only has six public methods, AddModule, AddUIModuleProvider, CreateApplication, RemoveApplication, RemoveModule, and RemoveUIModule. The other methods are just helper methods to facilitate reading configuration.

using System;
using Microsoft.Web.Administration;

namespace SetupHelper {

    public static class InstallUtil {

        /// <summary>
        /// Registers a new Module in the Modules section inside ApplicationHost.config
        /// </summary>
        public static void AddModule(string name, string type) {
            using (ServerManager mgr = new ServerManager()) {
                Configuration appHostConfig = mgr.GetApplicationHostConfiguration();
                ConfigurationSection modulesSection = appHostConfig.GetSection("system.webServer/modules");
                ConfigurationElementCollection modules = modulesSection.GetCollection();

                if (FindByAttribute(modules, "name", name) == null) {
                    ConfigurationElement module = modules.CreateElement();
                    module.SetAttributeValue("name", name);
                    if (!String.IsNullOrEmpty(type)) {
                        module.SetAttributeValue("type", type);
                    }

                    modules.Add(module);
                }

                mgr.CommitChanges();
            }
        }

        public static void AddUIModuleProvider(string name, string type) {
            using (ServerManager mgr = new ServerManager()) {

                // First register the Module Provider
                Configuration adminConfig = mgr.GetAdministrationConfiguration();

                ConfigurationSection moduleProvidersSection = adminConfig.GetSection("moduleProviders");
                ConfigurationElementCollection moduleProviders = moduleProvidersSection.GetCollection();
                if (FindByAttribute(moduleProviders, "name", name) == null) {
                    ConfigurationElement moduleProvider = moduleProviders.CreateElement();
                    moduleProvider.SetAttributeValue("name", name);
                    moduleProvider.SetAttributeValue("type", type);
                    moduleProviders.Add(moduleProvider);
                }

                // Now register it so that all Sites have access to this module
                ConfigurationSection modulesSection = adminConfig.GetSection("modules");
                ConfigurationElementCollection modules = modulesSection.GetCollection();
                if (FindByAttribute(modules, "name", name) == null) {
                    ConfigurationElement module = modules.CreateElement();
                    module.SetAttributeValue("name", name);
                    modules.Add(module);
                }

                mgr.CommitChanges();
            }
        }

        /// <summary>
        /// Create a new Web Application
        /// </summary>
        public static void CreateApplication(string siteName, string virtualPath, string physicalPath) {
            using (ServerManager mgr = new ServerManager()) {
                Site site = mgr.Sites[siteName];
                if (site != null) {
                    site.Applications.Add(virtualPath, physicalPath);
                }
                mgr.CommitChanges();
            }
        }

        /// <summary>
        /// Helper method to find an element based on an attribute
        /// </summary>
        private static ConfigurationElement FindByAttribute(ConfigurationElementCollection collection, string attributeName, string value) {
            foreach (ConfigurationElement element in collection) {
                if (String.Equals((string)element.GetAttribute(attributeName).Value, value, StringComparison.OrdinalIgnoreCase)) {
                    return element;
                }
            }

            return null;
        }

        public static void RemoveApplication(string siteName, string virtualPath) {
            using (ServerManager mgr = new ServerManager()) {
                Site site = mgr.Sites[siteName];
                if (site != null) {
                    Application app = site.Applications[virtualPath];
                    if (app != null) {
                        site.Applications.Remove(app);
                        mgr.CommitChanges();
                    }
                }
            }
        }

        /// <summary>
        /// Removes the specified module from the Modules section by name
        /// </summary>
        public static void RemoveModule(string name) {
            using (ServerManager mgr = new ServerManager()) {
                Configuration appHostConfig = mgr.GetApplicationHostConfiguration();
                ConfigurationSection modulesSection = appHostConfig.GetSection("system.webServer/modules");
                ConfigurationElementCollection modules = modulesSection.GetCollection();
                ConfigurationElement module = FindByAttribute(modules, "name", name);
                if (module != null) {
                    modules.Remove(module);
                }

                mgr.CommitChanges();
            }
        }

        /// <summary>
        /// Removes the specified UI Module by name
        /// </summary>
        public static void RemoveUIModuleProvider(string name) {
            using (ServerManager mgr = new ServerManager()) {
                // First remove it from the sites
                Configuration adminConfig = mgr.GetAdministrationConfiguration();
                ConfigurationSection modulesSection = adminConfig.GetSection("modules");
                ConfigurationElementCollection modules = modulesSection.GetCollection();
                ConfigurationElement module = FindByAttribute(modules, "name", name);
                if (module != null) {
                    modules.Remove(module);
                }

                // now remove the ModuleProvider
                ConfigurationSection moduleProvidersSection = adminConfig.GetSection("moduleProviders");
                ConfigurationElementCollection moduleProviders = moduleProvidersSection.GetCollection();
                ConfigurationElement moduleProvider = FindByAttribute(moduleProviders, "name", name);
                if (moduleProvider != null) {
                    moduleProviders.Remove(moduleProvider);
                }

                mgr.CommitChanges();
            }
        }
    }
}