Custom Installer Actions: Edit Connection Strings, IIS Directory Security Settings, etc.

When I need to deliver a web application to a client, I create a Setup/Deployment Project and then send them the .MSI file.  This makes it super easy to make sure that all the dependencies are there and that the web directory gets set up in IIS.  It eliminates 80% of the deployment hassle. 

The remaining 20% of the deployment hassle comes from having to tweek database connection strings in different files and updating other configuration settings.  In order to do this with the Deployment Project in VS2005, you need to implement a Custom Installer Action.

I finally got around to doing writing one this week.  

Here's what it can do:

  • Edit database connection strings through a dialog that pops up during the installation
  • Types of connection strings supported: NHibernate (hibernate.cfg.xml), Stand-alone Enterprise Library (*.config) files, Web.config "connectionString" values, Web.config appSettings-based "add" connection strings
  • Modify IIS Directory Security Settings (Allow NTLM, Allow Basic, Allow Anonymous)
  • Update paths to configuration files referenced in Web.config (eg. custom external configuration files)

I've posted two zip files: the source code and the binaries.  The source code includes the code for the installer actions, the VS2005 unit tests, a sample web application, and a sample installer for the web application.  If you're looking for an example of how to edit IIS from C#, IISActiveDirectoryUtility.cs inside of the Com.Benday.InstallerActions project is a great place to start. 

If you want to use the custom action binaries in your own installer project, you need to:

  1. In Solution Explorer, click on your web application deployment project
  2. Click the "File System Editor" button (at the top of Solution Explorer)
  3. Go to the "bin" directory inside the "Web Application Folder"
  4. Right-click the "bin" folder and choose Add --> File...
  5. Choose Com.Benday.InstallerActions.dll (from wherever you put it)
  6. Now click on the "Custom Actions Editor" button (at the top of Solution Explorer)
  7. Right click on the "Install" folder and add one or more references to Com.Benday.InstallerActions.dll
  8. Edit the CustomActionData value in the properties for each reference to Com.Benday.InstallerActions.dll according to the documentation later in this post

Your Custom Actions screen should now look something like this:

And after you've configured it, the properties for each should look something like this:

CustomActionData arguments to edit the IIS directory security settings:

  • /action=VirtualDirectorySecurity
  • /virtualDirectory="[TARGETVDIR]"
    [TARGETVDIR] is a reserved variable that is supplied by the installer that tells you the name of the IIS directory
  • /AllowNTLM=1 or /AllowNTLM=0
    To enable NTLM on the directory set this value to 1.  To disable NTLM, set this value to 0 or omit /AllowNTLM
  • /AllowBasic
    To enable basic authentication on the directory set this value to 1.  To disable, set this value to 0 or omit.
  • /AllowAnonymous
    To enable anonymous access on the directory set this value to 1.  To disable, set this value to 0 or omit.

CustomActionData arguments for editing NHibernate database connection strings:

  • /action=ConfigurationFiles
  • /targetDirectory="[TARGETDIR]\"
    [TARGETDIR] is a reserved variable that is supplied by the installer that points to the filesystem path for the directory. HINT: make sure that you put this in quotes and that you have a "\" after [TARGETDIR].  If you see weird FileNotFound errors, this is probably the culprit. 
  • /hibernateConfigFile=hibernate.cfg.xml
    This is the name of the NHibernate configuration file.  This value will always be "hibernate.cfg.xml".

CustomActionData arguments for editing connection strings stored in Web.config's "connectionStrings" element:

  • /action=ConfigurationFiles
  • /targetDirectory="[TARGETDIR]\"
    [TARGETDIR] is a reserved variable that is supplied by the installer that points to the filesystem path for the directory. HINT: make sure that you put this in quotes and that you have a "\" after [TARGETDIR].  If you see weird FileNotFound errors, this is probably the culprit. 
  • /webConfigConnectionString=WebConfigConnectionString
    This is the "name" of the connection string that you want to edit

CustomActionData arguments for editing connection strings stored in Web.config's "appSettings" element:

  • /action=ConfigurationFiles
  • /targetDirectory="[TARGETDIR]\"
    [TARGETDIR] is a reserved variable that is supplied by the installer that points to the filesystem path for the directory. HINT: make sure that you put this in quotes and that you have a "\" after [TARGETDIR].  If you see weird FileNotFound errors, this is probably the culprit. 
  • /webConfigDatabaseKeys=DB_CONNECTION;ANOTHER_DB_CONNECTION
    The value is the name of the "add" element that has the connection string in it.  If you have multiple "add" elements with database connection strings in them, separate the values by semi-colon.

CustomActionData arguments for editing connection strings stored in a separate Enterprise Library Database Configuration file:

  • /action=ConfigurationFiles
  • /targetDirectory="[TARGETDIR]\"
    [TARGETDIR] is a reserved variable that is supplied by the installer that points to the filesystem path for the directory. HINT: make sure that you put this in quotes and that you have a "\" after [TARGETDIR].  If you see weird FileNotFound errors, this is probably the culprit. 
  • /enterpriseLibraryDbConfigFile=dataConfiguration.config
    This is the name of the file that has the enterprise library configuration data

CustomActionData arguments for editing paths to files referenced in Web.config's appSettings:

  • /action=ConfigurationFiles
  • /targetDirectory="[TARGETDIR]\"
    [TARGETDIR] is a reserved variable that is supplied by the installer that points to the filesystem path for the directory. HINT: make sure that you put this in quotes and that you have a "\" after [TARGETDIR].  If you see weird FileNotFound errors, this is probably the culprit. 
  • /webConfigUpdatePathToFilenamesKey=PATH_TO_A_FILE;SomeImportantFile.txt
    This is a semi-colon delimited value.  The first part of the value is the name of the "add" key in "appSettings".  The second part is the name of the file that needs to have it's full filesystem path updated.  If you need to update more than one file reference, you can add more semi-colon delimited pairs. 

More Hints:

If you have several database config values that that all need to point to the same database -- for example, some codee connects via Enterprise Library and some code uses hibernate.cfg.xml -- put all of these into the same /action=ConfigurationFiles reference.  A CustomActionData that looks like this "/action=ConfigurationFiles /targetDirectory="[TARGETDIR]\" /enterpriseLibraryDbConfigFile=dataConfiguration.config /hibernateConfigFile=hibernate.cfg.xml" would allow you to set the enterprise library db config file and the nhibernate config file to the same database connection at the same time. 

Definitely take a look at the custom action configuration in the sample installer that's included in the source code zip.  After that, feel free to post your questions.

-Ben

COMException When Trying to Access Any Member For an IIS DirectoryEntry

I've been playing around with writing a Custom Installer Actions for a VS2005 Web Application Deployment Project (aka Web Application Installer).  The most challenging thing after figuring out the syntax weirdnesses for Custom Installers was manipulating IIS from C#. 

There are a ton of blog entries out there on how to manipulate IIS from .NET but I was still having problems getting the code to work.  The big error that I kept getting was

System.Runtime.InteropServices.COMException: The system cannot find the path specified.

This error showed up whenever I tried to access any property or method on the DirectoryEntry object for the IIS directory. The error even showed up when I was in the debugger. 

The solution came from Saar Carmi's blog post.  All the other posts out there got it MOSTLY right but Saar's got this one little extra line in there:

folderRoot.RefreshCache();

That's it.  That's the line that makes EVERYTHING work.  Call RefreshCache() on the DirectoryEntry after you've created it and suddenly all those properties work.  (Thanks, Saar!)

Here's a working piece of code piece of code:

public static DirectoryEntry FindDirectoryForPath(string appDirectory)
{
	string machineName = System.Environment.MachineName;
	string query;

	query = String.Format("IIS://{0}/w3svc", machineName);

	DirectoryEntry w3svc = new DirectoryEntry(query);

	DirectoryEntry appDirectoryEntry = null;

	foreach (DirectoryEntry webSite in w3svc.Children)
	{
		query = String.Format(
			"IIS://{0}/W3SVC/{1}/ROOT/{2}",
			machineName, webSite.Name, appDirectory);

		try
		{
			appDirectoryEntry = new DirectoryEntry(query);

			if (appDirectoryEntry != null)
			{
				appDirectoryEntry.RefreshCache();
				return appDirectoryEntry;
			}
		}
		catch (COMException)
		{

		}
		catch (Exception)
		{
			// not this one
		}
	}

	// couldn't find anything
	return null;
}

-Ben

Migrating VS2003 Web Applications to VS2005 Web Application Projects

I just spent a couple of days migrating my remaining (important) VS2003 Web Application code to VS2005.  It had its frustrating moments but each of those frustrating moments reminded my why it was such a great idea to dump the VS2003 Web App code.  The #1 biggest problem was getting VS2003 Web Applications to actually open on a machine other than the one it was written on (remember how much fun it was to get the IIS virtual directory settings to match with the file system directory references in the .sln file?)

Anyway, it's done. 

Here are the top weird issues:

1. Problem: You run your migrated Web Deployment Project (aka Installer Project or MSI) and then go to the newly created website and there's nothing in the "bin" directory.  All the assemblies (DLLs) are in the root of the website. 

In VS2003 installers, when you specified the "Primary Output" from a web app, you'd put it in the root of the "Web Application Folder".  This Primary Output would create a "bin" directory and copy it's output into that directory.  (I always thought this was a little confusing.)  It doesn't do this anymore in VS2005.

Solution: Create a "bin" folder in the "Web Application Folder" and move (drag/drop) the Primary Output from your web project into "bin".  When you've done this, you should see "Primary output from MyWebProject (Active)" in the "bin" directory along with all the assemblies that it depends on.  In the "Web Application Folder" you should just have "Content Files from MyWebProject (Active)". 

2. Problem: Page_Load() isn't getting called.

This happened once or twice to me when migrating.  The conversion process for 2003 to 2005 has to go through your code and create the MyPage.aspx.designer.cs files and the "partial" keyword.  If you're doing the conversion in a folder other than the original, the converter gets confused and edits some files in the wrong folder (translated: it edits the wrong file). 

Solution: for any file that isn't working, go into the .aspx.cs file (MyPage.aspx.cs) and find the InitializeComponent() method.  (Hint: It's inside of a #region named "Web Form Designer generated code" and is covered with code comments that direct you to never ever modify this code.)  Make sure that there's a line in there that subscribes your Page_Load() to the Load event for the Page. 

private void InitializeComponent()
{
    this.Load += new System.EventHandler(this.Page_Load);
}

3. Problem: AuthenticateRequest() isn't getting called and your custom security code doesn't work.

This is another one that's kind of like problem #2 -- essentially the events aren't getting hooked up properly in global.asax.cs.  One of the things that I saw is that sometimes my global.asax.cs's had two methods in them: Global_AuthenticateRequest() and Application_AuthenticateRequest().  Sometimes there'd be code

Solution(?): This one was extra difficult.  Maybe I'm just being cranky here but I didn't think that the documentation was all that great about how to hook up to events in the global.asax of a VS2005 Web Application Project (I've done it in VS2005 Web Site projects before -- the documentation was somewhat hazy there, too.) 

It looks like the easiest way to solve this problem is to name your AuthenticateRequest method "Application_AuthenticateRequest" and be done with it.

protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
    Context.User = new SecurityPrincipal(new SecurityIdentity());
}

If you do this, make sure that you don't have any explicit subscriptions to the AuthenticateRequest event inside of the Global() constructor -- if you name your method Application_AuthenticateRequest() and you also subscribe to the AuthenticateRequest event, then your auth code is going to get called twice.  Check to see if you have something similar to this:

public Global()
{
  InitializeComponent();
  this.AuthenticateRequest+=new EventHandler(Global_AuthenticateRequest);
}

If you see that "this.AuthenticateRequest+=" line in your code and you already have an Application_AuthenticateRequest() method, you probably want to delete the "this.AuthenticateRequest+=" line.

-Ben