Pages

Thursday, August 16, 2012

Using Web.config Transforms to make multi-environment deployments easy


When tasked with configuration management responsibilities, you normally have several environments to which you must deploy your systems and most often, your web.config files have different information for each environment.  Several approaches have been used to address the problem this situation creates such as using T4 (Text Template Transformation Toolkit) or simply creating a separate configuration file for each environment.  I’ve personally used the latter most often as it is a quick and simple solution to the problem and works well for Click Once deployments too.  The challenge with this, however (especially in a team environment), is keeping the config files in sync when you add or change something- most developers forget to update the rest of the files and then deployments become a nightmare.

Enter the wonderful world of Web.config transforms.  In the spirit of keeping this short, I’m not going to cover everything about how the transforms work.  There is great reference documentation from Microsoft here:http://msdn.microsoft.com/en-us/library/dd465326(VS.100).aspx

We have to start with a quick overview of Web Projects.  When building web applications with Visual Studio, there are two types of projects one may chose:  Web Application or Web Site.  A discussion on which type of project is “better” is a matter for another day.  A great article exploring this question may be found here:http://vishaljoshi.blogspot.com/2009/08/web-application-project-vs-web-site.html

A quick summary of the some important points, however, is this.  Web Sites do not require precompilation and may therefore be updated “on the fly” allowing for quick hot-fixes in production environments.  It is, of course, debatable as to whether or not that is a good thing.  The downside is they don’t take advantage of Web.config transformations and all of the other great tools Microsoft is making available like the Web Platform Installer.  Web Applications, on the other hand, do take advantage of many of these great tools.  The downside is that you must recompile the code if you make any changes making debugging a bit more cumbersome and and quick fixes require full blown deployments.  Again, there are many merits and many flaws to both choices, but I want to review the wonderful new world of Web.config Transforms found in Web Application Projects.


Publish Profile & Configurations

Web Application Projects have a new Profile feature in the Publish functionality.  Right click on the project and chose Publish, like so:

image

image 

There are two important aspects to this example: 1) Profile Name and 2) Build Configuration.  I’m not going to talk about all of the Publish Methods available because this too is a topic for another post.  For now, my publish method of choice is “File System”.

Profile

Simply create a profile for each of your environments.  Visual Studio stores these in a publish.xml file and are kept locally.  You can add these to source control of course if you want to make them available to your team.  The way I use the File System methodology is this:  “Dev” Profile = UNC path of the website on the dev server, “Test” Profile = UNC path of the website on the test sever, etc.

image

Configuration

Let’s assume for this example you have 4 environments: 1) Local Dev, 2) Development, 3) Testing, and 4) Production.  Use the Configuration Manager to create a Configuration for each of your environments.  “Debug” works for Local Dev and “Release” works for Production, so let’s create two new configurations for Dev and Test.  Note that my “Dev” environment is a development server, not my local machine.

image

Config Transforms

Now that that we have Configuration for each of the environments, it’s time to add the transform files to the web.config.  Right click on your Web.config file and you’ll see a spiffy option called “Add Config Transoms”.  Click this and poof, Visual Studio will add transforms for each of your new Configurations.

image

The result:

image

Refer to the MSDN article for full details of the transform language, but it’s quite simple to replace data.  The easiest things to do are matching a specific key (such as a connection string) or replacing an entire element.  The greatest thing about Web.config transforms is that you only need to keep the deltas in the transform files, but using replace works well if you don’t want to learn the fine points of the transformation language (I use this for my Enterprise Library configuration blocks).

Replacing a Connection String

Replacing a connection string is done like this (forgoing the obvious discussion of how connection strings should always be encrypted):
<add name="Axapta.Data.Properties.Settings.AxaptaConnectionString" 
connectionString="Data Source=XX;Initial Catalog=ax50_test;uid=XXX;pwd=YYY!@#QWE"providerName="System.Data.SqlClient" 
xdt:Transform="SetAttributes" 
xdt:Locator="Match(name)" />
 

Replacing an entire Element

<loggingConfiguration name="Logging Application Block" tracingEnabled="true"
defaultCategory="" logWarningsWhenNoCategoriesMatch="true"
xdt:Transform="Replace">
        ...
</loggingConfiguration>

image

Deploying

Once you get everything setup, you’re basically done.  Now when you use the Publish functionality, Visual Studio will apply the transforms to your deployed Web.config based on your active configuration.  Thus, deploying becomes a three step process: 1) Select your configuration in the Configuration Manager, 2) Select your Profile in the Publish Tool, and 3) Click Publish!

GiddyUp, EzPz, etc…  <enter choice words of happiness here>

One might be able to write a small book on the topics covered in this article, but hopefully I kept it short and sweet so you can use it to get jump started or as a quick reference.

Happy coding,

Orca MSI Editor

I had to use a program that Microsoft puts out for free today. It is called  and it can be used to edit MSIs at the database level. It is a good tool, but to get it you have to download like 300 megs of junk from MS. It seemed pretty dumb, so I am posting here so that I don’t have to do that anymore. 
Download orca.msi

Custom VS Web Setup Projects

A Web Setup project installs an ASP.NET web application on a Microsoft Internet Information Services (IIS) web server. The simplest web application might consist of Default.aspx and Default.aspx.cs files; a simple web setup project would install these files into a directory of the installer's choice and mark that directory as an IIS application, eg so that the web app runs at http://localhost/installdir/

The Visual Studio 2008 Web Setup project looked like a good starting point for distributing a new version of my web application (with web service and a Forms app that tests the web service). I just needed to customise it to:
  • Add links to the web app in the Start Menu (eg to http://localhost/installdir/default.aspx)
  • Add links to the web app on the install wizard finished page
  • Set NTFS write permissions for the web app directory
  • [Later] Make the MSI install work in Vista
Little did I know what a task I had set myself. Perhaps buying an installation tool might have been worthwhile... Follow me, as I battle to achieve these modest aims.
My software is aimed at individual users and to run on servers. Running as an ASP.NET web app, it could fulfil both target systems. I want to have Start Menu links so that individuals have a means to access the web site easily. The Start Menu would also have a shortcut to a Windows Forms exe that would test the web servce.
Basic set up: Files, Program Files and Start Menu
First, make sure that you are targetting the right version of ASP.NET - you can choose this at the top right of the VS "Add New Project" dialog. Once your project is created, look at the project properties. Click on [Prerequisites] and make sure that your chosen base version is selected. I selected ".NET Framework 2.0 (x86)" and "Windows Installer 3.1".
Next, add the project output from your web app project. View the File System and add in your aspx pages (but not .aspx.cs), Global.asax and release Web.Config files, together with any other files you want in the web app directory. Make sure that the bin directory has all the needed assembly DLLs.
My Windows Forms application had best go in the Program Files directory, so I added a "Program Files Folder" special folder to the setup File System. I created a "Product" sub-folder for my software, and added the Forms app output to this folder. [Later on, I also put my custom installer to this folder.]
I added a "User's Start Menu" special folder to the setup File System. I added "Programs" then another "Product" folder inside. I want to add my URL links to the web app in here, but I had to do that in my custom installer. However I did create a shortcut to the Forms app. I had wanted to pass the actual web app URL as a parameter to this app, but this proved not to be possible, so I amended the app to get the URL from a file.
MSI Database overview
The web setup project creates a Windows Installer MSI file, and an accompanying Setup.exe. An MSI file has a real relational database inside that governs how the install proceeds. The database has a series of tables each with rows with various columns.
For example, the Dialog table has one row to define each dialog that can appear during the install. The Controltable has rows that define each user interface element in the dialogs.
You can see what's in the MSI database using the orca tool, available in the Windows SDK. Later on, I'll use orca to build a Transform that changes the main text of the finished dialog box. The msitran tool can then be used as the setup PostBuildEvent to run this transform after every project rebuild.
The install process maintains various properties, some of which are defined in the Property table. Other properties are set at install time - these are the ones I am interested in:
  1. [TARGETVDIR]: The chosen virtual directory, eg Product
  2. [TARGETDIR]: The consequent web app path, eg C:\inetpub\wwwroot\Product
  3. [StartMenuFolder]: The Start Menu, eg C:\Documents and Settings\All Users\Start Menu
NB: There are various Win32 functions that let you work with the MSI database, such as MsiOpenDatabase().
Custom Action Installer NET assembly
You can achieve some things in a .NET custom action assembly, but your code is quite isolated from the main install. I don't think you can use/edit the current MSI database, nor can you interact with any of the setup dialogs.
To create a custom installer, create a C# class library project - give it a useful name. Then delete the initial class1.cs. Add a new item "Installer Class" and give the file and class a meaningful name. Switch to code view. You will see that your class inherits from System.Configuration.Install.
There are various overrides that you can add. You will definitely want to override Install() and possibly Commit(). You will almost certainly also want to override Rollback() and Uninstall() - to undo any changes that you have done. In fact, you must override and call Install() even if you only want to work in the other methods.
For each override that you add, add a call to the base class at the start of your implementation, eg:
public override void Install(IDictionary stateSaver)
{
  base.Install(stateSaver);
}
Compile this project and add the output to the web setup project File System Program Files "Product" sub-folder.
Note: if an exception is thrown in your Install() method, the install will rollback.
Web setup Custom Actions
In the web setup project, view the Custom Actions screen. Add a Custom Action for each of the types that you wish to support. Make sure that the InstallerClass property is True. For the Install action, set the CustomActionData as follows:

/TargetDir="[TARGETDIR]\" /TargetVDir="[TARGETVDIR]" /StartMenuFolder="[StartMenuFolder]\"
This sets three parameters that are available to your custom installer assembly. Note the double quotes, and that the backslashes that seem to mandatory in two cases.
Installer custom action
In your Installer() code, use the base class Context.Parameters StringDictionary to retrieve the values passed in the CustomActionData:
string StartMenuFolder = Context.Parameters["StartMenuFolder"];
string TargetDir = Context.Parameters["TargetDir"];
string TargetVDir = Context.Parameters["TargetVDir"];
Getting the web app URL
The URL of the installed web app will probably be http://localhost/product/ where product is the user's chosen virtual directory, as supplied in TargetVDir. However I want to try to check this. This code gets the path that should correspond to http://localhost/:
DirectoryEntry localhost1 = new DirectoryEntry(@"IIS://localhost/W3SVC/1/Root");
string localhostRoot = localhost1.Properties["Path"][0].ToString();
If this directory is C:\inetpub\wwwroot and the TargetDir is C:\inetpub\wwwroot\Product\ then I think we can be fairly certain that appending TargetVDir to http://localhost/ will give the correct URL of your installed web app. This code sets MakeLocalhostLinks to true if we can safely create URL shortcuts:
bool MakeLocalhostLinks = true;
if ((String.Compare(localhostRoot, TargetDir.Substring(0, localhostRoot.Length), true) != 0) ||
  (String.Compare(@"\" + TargetVDir + @"\", TargetDir.Substring(localhostRoot.Length), true) != 0))
{
  MessageBox.Show("Could not create Start Menu links", "Installer");
  MakeLocalhostLinks = false;
}
Add URL links to the Start Menu
I can now find our Start Menu folder that has just been created and add links there. A link is a text file with extension .url that has one line containing [InternetShortcut] and the next specifying the link after URL=
The following code does this job, creating a link to begin.aspx on the web app site, as well as creating a fixed link to the product documentation. In both cases, the path to the newly created link is saved in the stateSaver IDictionary, for use later on rollback or uninstall.
string UserStartMenuFolder = StartMenuFolder + @"Programs\product\";
if (Directory.Exists(UserStartMenuFolder))
{
  if (MakeLocalhostLinks)
  {
    string BeginLinkPath = UserStartMenuFolder + @"Begin.url";
    using (StreamWriter twBeginLink = new StreamWriter(BeginLinkPath))
    {
      twBeginLink.WriteLine("[InternetShortcut]");
      twBeginLink.WriteLine("URL=http://localhost/" + TargetVDir + "/begin.aspx");
    stateSaver.Add("BeginLinkPath", BeginLinkPath);
    }
  }
  string InfoLinkPath = UserStartMenuFolder + @"Product documentation.url";
  using (StreamWriter twInfoLink = new StreamWriter(InfoLinkPath))
  {
    twInfoLink.WriteLine("[InternetShortcut]");
    twInfoLink.WriteLine("URL=http://www.example.com/product/");
    stateSaver.Add("InfoLinkPath", InfoLinkPath);
  }
}
Commit
Commit is called when the install is complete. You cannot make changes to the stateSaver during Commit()
Commit() could be a good time to present some extra choices to the user, start an application or show a web page. Note that the installer waits for your method to return before continuing. You could spawn another process if you want to leave a dialog box open.
Rollback and Uninstall
If anything goes wrong with the install, or an uninstall occurs, you need to undo any changes you have made. InRollback() and Uninstall() you have access to the values that were saved in stateServer. Both Rollback() and Uninstall()call my own new method RemoveCustomAdditions(). This gets the saved path strings; if they are present then the file is deleted.
public override void Rollback(IDictionary stateSaver)
{
  base.Rollback(stateSaver);
  RemoveCustomAdditions(stateSaver);
}
public override void Uninstall(IDictionary stateSaver)
{
  base.Uninstall(stateSaver);
  RemoveCustomAdditions(stateSaver);
}
private void RemoveCustomAdditions(IDictionary stateSaver)
{
  try
  {
    string BeginLinkPath = stateSaver["BeginLinkPath"] as string;
    if (!String.IsNullOrEmpty(BeginLinkPath))
      File.Delete(BeginLinkPath);

    string InfoLinkPath = stateSaver["InfoLinkPath"] as string;
    if (!String.IsNullOrEmpty(InfoLinkPath))
      File.Delete(InfoLinkPath);
  }
  catch (Exception ) { }
}
During rollback or uninstall it is best if you don't throw any exceptions, so catch and ignore these.
My web app may create data files during normal operation. The rollback/uninstall methods could remove these files. However, an upgrade installation will usually uninstall the software first (even if RemovePreviousVersions is false). To keep the data files through an upgrade, these must be left in place. My documentation tells the user to remove these data files for a complete uninstall.
There should be no need to undo any file permission changes as the directory should be about to disappear.
NTFS File Permissions
In my Install() method, I want to let my web app write to its directory and any sub-folders. In IIS5, aspnet_wp is the worker process that runs all ASP.NET web apps using username "ASPNET". In IIS6 and IIS7, the w3wp worker process runs ASP.NET web app application pool using username "Network Service". This is the code that changes the file permissions for both these user accounts for the TargetDir:
DirectorySecurity security = Directory.GetAccessControl(TargetDir);
try
{
  FileSystemAccessRule access = new FileSystemAccessRule("ASPNET",
    FileSystemRights.Modify,
    InheritanceFlags.ContainerInherit InheritanceFlags.ObjectInherit,
    PropagationFlags.None,
    AccessControlType.Allow);
  security.AddAccessRule(access);
  Directory.SetAccessControl(TargetDir, security);
}
catch (Exception ) { }

try
{
  FileSystemAccessRule access = new FileSystemAccessRule("Network Service",
    FileSystemRights.Modify,
    InheritanceFlags.ContainerInherit InheritanceFlags.ObjectInherit,
    PropagationFlags.None,
    AccessControlType.Allow);
  security.AddAccessRule(access);
  Directory.SetAccessControl(TargetDir, security);
}
catch (Exception ) { }
Changing the text in the final finished wizard page
I had originally wanted to provide a means of starting the web app on the final wizard page. So far I have not found a way to do this (either provide links/buttons on the page, or put checkboxes with options that are actions when Finish is pressed).
However I did manage to update the text to include the words "Now click on the Product links in the Start Menu".
Using the Microsoft SDK orca tool, I opened a first cut of my MSI. Create a New Transform. In the Control table, find the row for FinishedForm BodyText. Right-click on the Text column and Copy Cell. Paste it into Notepad and alter the text as you want. Copy the text and Paste Cell back into the Text cell. Select Generate Transform and save as an .MST file. Note that you cannot use "\r\n" in the cell text although line breaks can be pasted.
Use the Microsoft SDK msitran tool to apply the transform to an MSI, ie in the PostBuildEvent for your setup project. I found that calling msitran directly caused the build to fail because it has a return code of 1. I therefore had to use a batch file that called msitran and then did something innocuous to clear the return code, eg:
msitran.exe -a ..\AlterSetupFinishedText.mst ProductSetup.msi
dir
Vista Installation
If you double-click on the MSI itself in Vista, then it will fail with an obscure message "The installed was interrupted before.." This is because of lack of administrator privileges, even if you are logged in as an Administrator.
The simplest solution to this problem is to run the Setup.exe program that the web app project creates - you are prompted by Vista UAC to OK this privilege escalation.
The alternative is to Run as Administrator a command prompt, and then enter msiexec /i yourInstaller.msi
And finally, sign your assemblies and MSI
Ensure that your project assemblies are signed before rebuilding the Web Setup project. Sign the MSI and associated Setup.exe.
Suggestion for Microsoft
Can you provide a means of accessing the current MSI database in .NET Installer assemblies please.

Creating Packaged ASP.NET Setup Programs with VS 2005


Scenario

You have built an ASP.NET Web Application using Visual Studio 2005, and want to enable customers to automatically install and deploy it on servers via an easy setup program.
Specifically, you want to create a standard Windows setup program that will create and configure the application on IIS, copy all of the application’s files to the appropriate location on the server, and ensure that ASP.NET 2.0 is correctly mapped to run the application. You also want the setup program to prompt the customer for the database location that the new application should use, and have the setup program automatically update the web.config file with the database connectionstring settings the customer provided.
One solution to consider using is the built-in "Web Setup Project" support that is built-in to Visual Studio 2005.  Web Setup Projects can be used to pipe the compilation outputs from VS 2005 Web Application Projects as well as Web Site Projects (when used with VS 2005 Web Deployment Projects), to create encapsulated Windows setup programs. The below walkthrough demonstrates step-by-step how to create and use one.

1) Create a VS 2005 Web Application Project

To begin with, we’ll start with an empty instance of Visual Studio and create a new VS 2005 Web Application project (select File->New Project->ASP.NET Web Application).  For the purposes of this simple sample we’ll have two pages in the project:
We’ll add a label to the Default.aspx page and a Page_Load event handler in the code-behind to output the current timestamp on each request. When I press F5 to build and run the application, the project will compile and run as I’d expect (and by default use the built-in VS Web Server):

2) Add a VS 2005 Web Setup Project to the Solution

Now that we have a simple ASP.NET application built, we’ll want to add a VS 2005 Web Setup Project to the solution. Choose the File->Add->New Project menu item to add one into your solution:
Note that the “Web Setup Project” type shows up under the “Other Project Types->Setup and Deployment” node in the New Project dialog above. Name it whatever you want and hit ok. It will then show up in your solution explorer as a separate project.
Our next step will be to configure the web setup project to take the compiled assemblies (\bin directory contents) + content markup (.aspx, .config, etc files) from our Web Application Project and use them as inputs within our setup project. To-do this, right-click on the web setup project node in the solution explorer and choose the “Add->Project Output” context menu item:
A dialog will then appear allowing us to select which project in the solution, and which of its project contents, we want to add to the setup package:
For ASP.NET Web Application Projects it is really important that we select both the “Primary Output” (which are the compiled assemblies for the \bin directory) as well as the “Content Files” (which are the .aspx markup files) within this dialog.
By default, the web setup project will copy both of these items into the root of the target Web Application Folder that the setup project will create. You can see that it is configured this way by opening up the “File System” view within the web setup project (right click on the web setup project root and choose View->File System):
This actually isn’t what we want to have happen though – since we really want the assemblies (indicated by the Primary Output node) to be copied into the application’s \bin directory instead (otherwise ASP.NET won’t be able to find them at runtime). To fix this, drag/drop the “Primary Output from MyApplication” node into the \bin directory. Once this is done you should be able to click on the “Web Application Folder” node on the left-hand side and see this:
And then click on the “bin” folder sub-node and see this:
We now have a basic web setup project created and configured for our ASP.NET Web Application. Next step is to build and run it.

3) Build and Run the VS 2005 Web Setup Project to the Solution

To build the web-setup project we can right-click on the web setup project node within the solution explorer and choose the “Build” option:
If you open the output window within VS (View->Output menu item), you will see the results of this build operation:
Our “MyApplicationSetup” project created a new MyApplicationSetup.msi Windows installer file and compressed and packaged the contents of our ASP.NET Web Application (note: in the web setup project properties dialog you can choose whether the compression algorithm used is optimized for size or speed).
Very Important: Because setup projects take awhile to build, they are by default marked not to build as part of the solution.  What this means is that you need to right-click on them and explicitly do a build in order for them to be recompiled.  Be careful to-do this when you make and test changes - otherwise you'll be running the previously compiled version and not the one with your latest code!
To test it, we can right-click on the web setup project within the solution explorer and choose the “Install” option to install it (or alternatively launch it outside of VS by running it):
This will launch a standard Windows installer and walk the user through installing the application on IIS:
VS 2005’s web setup projects allow you to pick which site to install the application on if multiple sites are configured on IIS (this wasn’t supported with the VS 2003 version). You can optionally specify an application virtual-directory path to use (for example: http://www.myserver.com/myapppath), or you can leave this value blank to install it as the root application on the site (for example: http://www.myserver.com/).
Once the installer completes, the application will have been copied to disk and registered with IIS. We can now run the application using the HTTP path we provided during installation like so:
Once installed the application will also show up in the standard “Add or Remove Programs” utility within the Windows Control Panel:
You can remove the application either by running uninstall from the control panel utility, or (at development time) by right-clicking on the web setup project node within the VS Solution Explorer and selecting the “Uninstall” menu item. This will cause all installed files to be removed from disk.

4) Update the Wizard UI of the Web Setup Project

By default the Windows installer created by a web setup project has some default instruction strings and banner images for the setup:
You can change this and customize the screens by right-clicking on the web setup project node in the VS solution explorer and selecting the "View->User Interface" context menu item):
This will then bring up a screen that shows the list of screens to be displayed during setup:
Unfortunately there isn't a forms-designer that you can use to override the screens above.  However, you can select a screen, and then go to the property grid to customize its text and change the graphics used within the screen:
You can also create new screens and add them into the setup wizard.  Later in this tutorial we'll use this feature to create a custom screen to collect database connection-string information and use it to automate configuring our web.config file to point at the appropriate database.

5) Adding Custom Actions to the VS 2005 Web Setup Project

Web Setup Projects contain built-in support for configuring and performing common setup actions. These include editors for adding/changing registry entries (choose View->Register to configure), changing file-type associations (View->File Types), and for validating prerequisite components are already installed (it automatically checks that the .NET Framework 2.0 redist is installed). Setup Projects also allow you to configure a number of common IIS settings declaratively (click on the “Web Application Folder” in the File System view and then look at the property grid to see these):
But for non-trivial setups you are likely to want to be able to execute your own custom code during setup to customize things. The good news is that web setup projects support this with something called “Custom Actions” – which is code you write that can execute during both install and uninstall operations.
To add a custom action you first want to add a new class library project to your solution (File->Add->New Project->Class Library). 
You then want to add assembly references in this newly created Class Library to the System.Configuration.Install.dll, System.Configuration.dll, System.Diagnostics.dll, and System.Web.dll assemblies. You’ll then want to create a new class for your custom action and have it derive from the “System.Configuration.Install.Installer” base class like so:
using System;
using 
System.Configuration.Install;
using 
System.ComponentModel;

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

            
// Todo: Write Your Custom Install Logic Here
        
}
    }
}
Notice the custom “RunInstaller(true)” attribute that must be set on the class name. This is important and required (and easy to forget!). You’ll need to add a using statement to the System.ComponentModel namespace to avoid fully qualifying this.
Next we’ll need to make sure this Custom Action assembly gets added to our web setup project. To-do this, right-click on the Web Setup Project root node in the solution explorer and select the View->File System menu item to bring up the file-system editor. Right-click on the “bin” sub-folder and choose “Add->Project Output” like we did earlier to get the custom action assembly added to the web setup project:
In this case we’ll want to select the Custom Action Class Library project instead of our web application one. Pick it from the project drop-down at the top of the dialog and then select the “Primary Output” option as the piece we want to add to the web-setup project (this will cause the Custom Action assembly to get added):
Lastly, we’ll configure the web-setup project to call our custom action assembly during the install phase of setup. To do this we’ll right-click on the web setup project root node in the solution explorer and choose the “View->Custom Actions” menu item. This will then bring up the Custom Actions Editor. Right-click on the “Install” node and choose “Add Custom Action”:
Drill into the Web Application Folder and Bin directory and choose the output from our Custom Action we imported:
The Setup Project will then automatically detect the custom action because of the “RunInstaller” attribute:
Our custom action class and Install method will now run anytime we run the installation setup program.

6) Useful Custom Action Example: ASP.NET Script Mapping Checker

The previous section showed how to create and configure an empty custom action class and install method. Let’s now do something useful with it. Specifically, let’s add code to verify that the right version of ASP.NET is correctly mapped for the application we are creating.
Because ASP.NET V1.1 and V2.0 can run side-by-side with each other on the same machine, it is possible to have different parts of a web server configured to run using different versions of ASP.NET. By default, the versions inherit hierarchically – meaning if the root application on a site is configured to still run using ASP.NET V1.1, a newly created application underneath the site root will by default run using V1.1 as well. What we’ll do in the steps below is write some code to ensure that our new application always runs using ASP.NET 2.0.
To begin with, we’ll select our custom action within the Custom Action explorer (just like in the previous screenshot above - using the View->Custom Action context menu item). We’ll then go to the property grid and specify a few parameters to pass to our custom action to use:
Specifically, we’ll pass in the target directory that the application is being installed in, the IIS site map path, and the IIS virtual directory name that the user specified in the setup wizard. This string of values looks like this:
/targetdir="[TARGETDIR]\" /targetvdir="[TARGETVDIR]" /targetsite="[TARGETSITE]"
We’ll then update our custom action to access these values and do something with them like so:
using System;
using 
System.Configuration;
using 
System.Configuration.Install;
using 
System.ComponentModel;
using 
System.Diagnostics;
using 
System.IO;

namespace 
MyCustomAction
{
    [RunInstaller(
true)]
    
public class ScottSetupAction : Installer
    {

        public override void 
Install(System.Collections.IDictionary stateSaver)
        {
            
base.Install(stateSaver);

            
// Retrieve configuration settings
            
string targetSite Context.Parameters["targetsite"];
            string 
targetVDir Context.Parameters["targetvdir"];
            string 
targetDirectory Context.Parameters["targetdir"];

            if 
(targetSite == null)
                
throw new InstallException("IIS Site Name Not Specified!");

            if 
(targetSite.StartsWith("/LM/"))
                targetSite 
targetSite.Substring(4);

            
RegisterScriptMaps(targetSite, targetVDir);
        
}

        
void RegisterScriptMaps(string targetSite, string targetVDir)
        {
            
// Calculate Windows path
            
string sysRoot System.Environment.GetEnvironmentVariable("SystemRoot");

            
// Launch aspnet_regiis.exe utility to configure mappings
            
ProcessStartInfo info = new ProcessStartInfo();
            
info.FileName Path.Combine(sysRoot, @"Microsoft.NET\Framework\v2.0.50727\aspnet_regiis.exe");
            
info.Arguments = string.Format("-s {0}/ROOT/{1}", targetSite, targetVDir);
            
info.CreateNoWindow = true;
            
info.UseShellExecute = false;

            
Process.Start(info);
        
}
    }
}
The above code launches the aspnet_regiis.exe utility that ships with ASP.NET within the \Windows\Microsoft.net\framework\v2.0.5.0727\ directory, and passes in the path location information for the site that the web setup installer previously created, along with the “-s” flag – which indicates that the IIS script-maps for that application should be updated to specifically use the ASP.NET 2.0 version, and not inherit the version number from any parent applications.
A special thanks to John for figuring this out in his blog post here.
Note: If you are using IIS6 or IIS7, you'll probably want to also add some logic into the custom action to ensure that the application pool that the application is being hosted in is also mapped to use ASP.NET 2.0.  Either that or you'll want to tell the admin to manually check the application pool settings after the setup is complete.

7) Useful Custom Action Example: Configuring Database Connection String

For our next custom action example, let’s add some UI to the setup that allows a user to configure the connection string details of a database the application should use.
Right click on the web setup project and open up the user interface screens again:
Right click on the "Install" node on the user interface screens page and chose to add a new dialog to the install wizard:
Chose one of the TextBox screens to use for gathering connection string details from the user:
Right-click on the TextBox screen node and move it up to be earlier in the wizard (right after we pick the IIS site and application name to use):
Then click on the TextBox screen and access its property window.  Via the property window you can change the text displayed on the screen, as well as control how many textboxes are visible:
Note in the above property window how I've set the Edit2, Edit3 and Edit4 text boxes to not be visible.  Now when we build and run the setup package we'll see this dialog in our wizard steps:
Now that we have UI to capture the connection-string value entered by a user in the wizard, we want to make sure it is passed to our custom action class.  You can do this by right-clicking on the web setup project node and by then choosing the "View->Custom Actions" context menu and then opening the property page window of our custom action:
We'll want to update the CustomActionData property value and pass in the connection-string of the database to use (we'll pass in the value from the EDITA1 textbox in the user interface screen):
/targetdir="[TARGETDIR]\" /db="[EDITA1]" /targetvdir="[TARGETVDIR]" /targetsite="[TARGETSITE]"
We can then update our custom action class to retrieve and use this connectionstring value to update the web.config file of the new application to contain the value the user installing the application entered. Below is a method that opens the web.config file for our new application and programmatically updates it with the user entered connection string:
void ConfigureDatabase(string targetSite, string targetVDir, string connectionString)
{
    
// Retrieve "Friendly Site Name" from IIS for TargetSite
    
DirectoryEntry entry = new DirectoryEntry("IIS://LocalHost/" + targetSite);
    string 
friendlySiteName entry.Properties["ServerComment"].Value.ToString();

    
// Open Application's Web.Config
    
Configuration config WebConfigurationManager.OpenWebConfiguration("/" + targetVDir, friendlySiteName);

    
// Add new connection string setting for web.config
    
ConnectionStringSettings appDatabase = new ConnectionStringSettings();
    
appDatabase.Name DATABASE_CONNECTION_KEY;
    
appDatabase.ConnectionString connectionString;

    
config.ConnectionStrings.ConnectionStrings.Clear();
    
config.ConnectionStrings.ConnectionStrings.Add(appDatabase);

    
// Persist web.config settings
    
config.Save();}
And now after we run the setup program our newly installed ASP.NET application's web.config file will have been updated to point to the right database.
To learn more about how the ASP.NET configuration APIs can be used to make changes to web.config files, please check out the management API section in the ASP.NET 2.0 Quickstart tutorials.  Chris Crowe also has some useful samples that demonstrate how to use the System.DirectoryServices APIs to query IIS settings (I needed them to figure out how to lookup the "friendly name" of the site from IIS to open up the web.config file).
You might also want to check out this MSDN documentation sample that demonstrates how to programmatically create a new database (complete with schema and data) with a custom action.  You could combine the approach in the MSDN article with the configuration one I used above to completely automate database deployment as part of your setup.

Summary

Hopefully the above tutorial helps demonstrate how to get started with using the built-in web setup project support within Visual Studio.  Click here to download a complete version of the sample I built above.
Web setup projects aren't perfect for all scenarios, and I'd primarily recommend them only for cases where you want a packaged GUI setup program (for example: to give to an external customer or to make available as a download on a web-site).  If you are instead working on maintaining/managing a site that you have direct access to, I'd probably instead recommend using the "Publish Application" feature available with VS 2005 Web Application Projects (for simple updates), or recommend authoring a PowerShell script to automate updates to the remote server.  For an example of a really advanced Powershell script thatwww.pageflakes.com uses to update their site, check out Omar's article here.
One downside with the VS 2005 Web Setup Project support is that you can only build web setup projects from within the IDE - which means you can't completely automate the creation of .MSIs as part of an automated MSBuild process.  If this is a showstopper for you, you should consider looking at the WIX setup framework- which does support this scenario. 
You can find a good set of WIX Tutorials here.  If someone wants to publish a blog post that demonstrates how to perform the scenarios I outlined in the blog post above using WIX, let me know and I will definitely link to it (and send you a few .NET books to say thanks!).
Hope this helps,

Modifying a Visual Studio web setup program to include a physical directory

When I was working on creating a new web application for a customer, I ran into a problem with the setup program. The default Web Setup project for VS2008 does not allow you to specify the physical install location for your application, but instead installs it in c:\inetpub\wwwroot. This wasn't going to work for my customer, who needed to be able to specify the physical directory. I could have used a regular setup project, which allows the end user to specify the physical install directory, but then the setup program wouldn't take care of creating the virtual directory in IIS. After some hunting, I found a solution that allows you to modify the setup MSI file so that the user can specify the physical install directory.

The process of modifying the MSI file is rather tricky, since it essentially involves modifying the MSI file once while Orca records your actions. I screwed it up the first time and had to start again. The shortcut is that you're welcome to use the MST file I created with this process to save yourself the trouble of creating the MST.

Now for the steps:

Obtain Orca.exe
Orca is part of the Windows SDK which can be downloaded at this link. After downloading and installing the SDK, browse to "Program Files\Microsoft SDKs\Windows\v7.0\Bin" and run Orca.msi to install the Orca utility.

Create the MST to be used to modify the MSI
These instructions are from this codeproject link. This assumes you've already created the MSI for the Web Setup project.
We'll use Orca to essentially record the changes you make to an MSI file; these changes will be saved in an MST file which you can then apply to other MSI files to perform the same changes.
  1. Run orca.exe
  2. Use File - Open to open the MSI
  3. Under Tools - Options - Database check Copy embedded streams during 'Save As'
  4. Under the Transform menu select New Transform to start recording a transform.
  5. In the screen shot image on the right (click to view it full-size) are shown 5 tables. Add the rows outlined in green to each table (Control, ControlEvent, CustomAction, InstallExecuteSequence, InstallUISequence).
  6. In the Control table, the Control_Next must refer to another control on that dialog, and all the controls must refer to each other in a loop. Since we added some extra controls to the WebFolderForm dialog, we need to modify one of the existing controls to refer to one of our new controls. Modify the AppPoolsCombo control (on the WebFolderForm dialog) and set theControl_Next column to be InstallLabel.
  7. We also have to remove some rows that would otherwise set the TARGETDIR back to the default:
    • In ControlEvent delete the row that has Dialog=WebFolderForm and Control="Next" and Event="DoAction"
    • In InstallExecuteSequence delete the WEBCA_EvaluateURLs action.
    • In InstallUISequence delete the WEBCA_EvaluateURLsNoFail action.
    • You're now done recording the Transform. From the menu choose Transform - Close Transform and save the transform (it will have a .MST extension).
A shortcut
This MST file was generated using the steps above for a VS2008 web setup project and should be able to be used as is to apply this transform to web setup MSI files.

Applying the transform
Now that you have an MST (transform) file, you need to use it to modify the setup MSI file.

You can apply a transform using Orca; open the MSI you want to transform, then from the menu choose Transform - Apply Transform, select the .MST file to use and it will modify the MSI. You can then save the MSI file.

You can also apply the transform via the command line using MsiTran.exe, which is installed with the Windows SDK (in Program Files\Microsoft SDKs\Windows\v7.0\Bin).

Applying the transform as part of a TFS (Team Foundation Server) build
In my case, I wanted to have this automatically done as part of the TFS (Team Foundation Server) build process we use. That's actually fairly simple. To apply the transform as part of a TFS build, copy the msitran.exe and your MST transform file to the build server. You can then modify the TFSBuild.proj file by adding an Exec such as the following to the AfterCompile target:

<Exec Command="c:\bin\msitran.exe -a c:\bin\WebSetupWithPhysicalDir.mst "$(OutDir)Setup\WrtServicesSetup.msi""/>

Results
When the setup program is installed on a Windows server, which can have multiple websites and application pools, the install screen should look something like this: