On my previous post, I discussed the requirement for an installer for an application created using the Smart Client Software Factory. I also discussed the peculiarities of the dynamic web references in library projects and how they are misleading since the entries in the app.config of the library projects need to be copied into the app.config of the Windows executable projects.
Some people have commented to me this is additional complexity brought on by working with Windows forms. I’d like to highlight this behavior
is a Visual Studio 2005 behavior. The same issue exists with ASP.NET applications. If you create a web reference in a library project instead of the web project, you will also need to copy the entries from the app.config of the library project into the web.config file.
This being said, let’s get into the code of the installer.
First, let’s use a helper class to deal with the various environments:
internal static class EnvironmentCode
{
public static string Development = “DEV”;
public static string UserAcceptanceTesting = “UAT”;
public static string SystemAcceptanceTesting = “SAT”;
public static string Production = “PROD”;
}
Let’s also create an application configuration class to handle configuration items related to the application. Notice the constructor needs to be passed an environment code and the assembly path. The assembly path is obtained from Context.Parameters[“assemblypath”] of the installer class that inherits from System.Configuration.Install.Installer. This will contain the path and file name of the installer Dll, not the application being
installed. That’s why the constructor takes this parameter and derives the target installation path of the application being installed. I removed the code comments for brevity here, but code comments are included in the code download.
internal class ApplicationConfiguration
{
private string _installationDirectory = String.Empty;
private string _exeConfigPath = String.Empty;
private string _sourceConfigPath = String.Empty;
public ApplicationConfiguration(string environmentCode, string
assemblyPath)
{
if (environmentCode == null)
throw new ArgumentNullException(“environmentCode”);
if (assemblyPath == null)
throw new ArgumentNullException(“assemblyPath”);
// (1) Set the installation directory
_installationDirectory = assemblyPath.Substring(0,
assemblyPath.LastIndexOf(@”\”));
// (2) Set the path to the executable file that
is being installed on the target
// computer.
We are expecting to find only one file that finishes in
“.exe”.
string[] applicationExecutablePath = Directory.GetFiles(_installationDirectory, “*.exe”);
if (applicationExecutablePath.Length != 1)
throw new InstallException(“Application
executable not found in installation directory.”);
// Set the name of the config file for the exe
(i.e. the application)
_exeConfigPath = applicationExecutablePath[0].ToString() + “.config”;
// (3) Set the path to the source configuration
file. Important assumption:
// The file must follow the naming convention
[EnvironmentCode].*.app.config
// and there must be only one file with the
environment code prefix.
if (environmentCode.ToUpperInvariant() != EnvironmentCode.Development)
{
string[] configFiles = Directory.GetFiles(_installationDirectory,
environmentCode.ToUpperInvariant() + “*.app.config”);
if (configFiles.Length == 1)
{
_sourceConfigPath = configFiles[0];
}
else
{
throw new
InstallException(environmentCode + “.*.app.config not found in “
+ _installationDirectory);
}
}
}
public string InstallationDirectory
{
get { return _installationDirectory; }
}
public string ExeConfigPath
{
get { return _exeConfigPath; }
}
public string SourceConfigPath
{
get { return _sourceConfigPath; }
}
}
Finally, the ApplicationInstaller class derives from the System.Configuration.Install.Installer class and overrides the Install method. Here is a diagram of the class:
The install method is pretty lean and calls discrete private methods like ProcessCustomActionData(). All of these methods are appropriate behaviors of the ApplicationInstaller class. The end result follows:
[RunInstaller(true)]
public partial class ApplicationInstaller :
System.Configuration.Install.Installer
{
private ApplicationConfiguration _applicationConfiguration;
private List<string> _configSectionsToEncrypt = new List<string>();
private String _encryptionProviderName = String.Empty;
private Boolean _encryptApplicationSettings = true;
public ApplicationInstaller()
{
InitializeComponent();
}
public override void Install(System.Collections.IDictionary stateSaver)
{
base.Install(stateSaver);
#if DEBUG
System.Windows.Forms.MessageBox.Show(“You may now attach the debugger to the managed msiexec instance. “ + “Click the OK button after you have
attached the debugger.”, “Installer Message”, System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Information);
#endif
ProcessCustomActionData();
CopyEnvironmentConfigToExeConfig();
EncryptExeConfigFile();
SecureEnvironmentConfigFiles();
return;
}
private void ProcessCustomActionData()
{
// Translate the environment code
string environmentCode = this.Context.Parameters[“ENVT”].ToUpperInvariant();
switch (environmentCode)
{
case “DEV”:
environmentCode = EnvironmentCode.Development;
break;
case “UAT”:
environmentCode = EnvironmentCode.UserAcceptanceTesting;
break;
case “SAT”:
environmentCode = EnvironmentCode.SystemAcceptanceTesting;
break;
case “PROD”:
environmentCode = EnvironmentCode.Production;
break;
default:
this.Context.LogMessage(“Unrecognized environment variable. The value passed was “ + environmentCode + “.”);
this.Context.LogMessage(“Defaulting to an installation for the development environment.”);
environmentCode = EnvironmentCode.Development;
break;
}
_applicationConfiguration = new ApplicationConfiguration(environmentCode,
this.Context.Parameters[“assemblypath”]);
// get Configuration section names from custom action parameter
// section names must be seperated by commas
String[] separator = { “,” };
String[] sectionNames = this.Context.Parameters[“sectionNames”].Split(separator, StringSplitOptions.None);
for (int i = 0; i < sectionNames.Length; i++)
{
_configSectionsToEncrypt.Add(sectionNames[i]);
}
//get Protected Configuration Provider name from custom action parameter
_encryptionProviderName = this.Context.Parameters[“provName”];
//get applicationSettings flag (decides whether or not the
//entire applicationSettings section group will be encrypted.
// If not provided, defaults to true.
String encryptApplicationSettings = this.Context.Parameter[“encryptApplicationSettings”];
if (encryptApplicationSettings == Boolean.FalseString)
_encryptApplicationSettings = false;
return;
}
private void CopyEnvironmentConfigToExeConfig()
{
if (_applicationConfiguration == null) throw new InstallException(“_applicationConfiguration cannot be null.”);
if (!String.IsNullOrEmpty(_applicationConfiguration.SourceConfigPath))
{
File.Copy(_applicationConfiguration.SourceConfigPath,
_applicationConfiguration.ExeConfigPath, true);
this.Context.LogMessage(“Succesfully copied “ +
_applicationConfiguration.SourceConfigPath +
” to “ +
_applicationConfiguration.ExeConfigPath);
}
return;
}
private void EncryptExeConfigFile()
{
if (_applicationConfiguration == null) throw new InstallException(“_applicationConfiguration cannot be null.”);
if (String.IsNullOrEmpty(_encryptionProviderName)) throw new InstallException(“CustomActionData must contain a provName.”);
// Map to application exe.config file.
ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
fileMap.ExeConfigFilename = _applicationConfiguration.ExeConfigPath;
Configuration configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap,
ConfigurationUserLevel.None);
if (_encryptApplicationSettings)
// Encrypt all sections in the applicationSettings section
group
{
ApplicationSettingsGroup
applicationSettings = (ApplicationSettingsGroup)configuration.GetSectionGroup(“applicationSettings”);
foreach (ConfigurationSection applicationSettingsSection in
applicationSettings.Sections)
{
if (applicationSettingsSection != null)
{
if (!applicationSettingsSection.SectionInformation.IsProtected)
{
applicationSettingsSection.SectionInformation.ProtectSection(_encryptionProviderName);
applicationSettingsSection.SectionInformation.ForceSave
= true;
configuration.Save(ConfigurationSaveMode.Full);
}
}
}
}
foreach (string sectionName in _configSectionsToEncrypt)
{
ConfigurationSection configurationSection;
try
{
configurationSection = configuration.GetSection(sectionName);
}
catch (Exception ex)
{
throw new InstallException(“The setup program must copy custom section handlers to the system directory.”, ex);
}
if (configurationSection != null)
{
if (!configurationSection.SectionInformation.IsProtected)
{
configurationSection.SectionInformation.ProtectSection(_encryptionProviderName);
configurationSection.SectionInformation.ForceSave
= true;
configuration.Save(ConfigurationSaveMode.Full);
}
}
}
return;
}
private void SecureEnvironmentConfigFiles()
{
if (_applicationConfiguration == null) throw new InstallException(“_applicationConfiguration cannot be null.”);
String[] environmentConfigFiles = Directory.GetFiles(_applicationConfiguration.InstallationDirectory, “*.app.config”);
for (int i = 0; i < environmentConfigFiles.Length; i++)
{
if (File.Exists(environmentConfigFiles[i]))
{
using (FileStream fs = new FileStream(environmentConfigFiles[i],
FileMode.Create, FileAccess.Write, FileShare.None))
{
this.Context.LogMessage(“Blanked out “ +
environmentConfigFiles[i]);
}
File.SetAttributes(environmentConfigFiles[i],
FileAttributes.Hidden);
}
}
return;
}
}
Key features to look for in the code:
-
The use of #if DEBUG means that when running the installation program with a DEBUG build, a popup window gices you a change to attach to the msiexec process from Visual Studio. There are several instances running; you will look for the one instance that is managed and has the title “Installer Message” as set in the code.
-
The CopyEnvironmentConfigToExeConfig method simply copies the source config file and overwrites the *.exe.config file. For example, if you’re deploying to SAT, SAT.app.config overwrites YourApplication.exe.config.
-
The environment specific config files can’t simply be deleted. If you do that, when you click on the shortcut to the applicationm the installer will attempt to repair the application. Instead, I wrote the SecureEnvironmentConfigFiles method to blank out the files and set them as hidden.
-
If custom section handlers are used (e.g. Enterprise Library), then the
section handlers must be copied to the system directory because the msiexec process executes from that directory and the CLR will be probing for the assemblies there. In the setup project, use the file system editor and include the requisite assemblies in the “System
Folder” folder. For example, to encrypt the securityConfiguration section (Enterprise Library), Microsoft.Practices.EnterpriseLibrary.Common.dll and Microsoft.Practices.EnterpriseLibrary.Security.dll are included in the System Folder. -
The Custom Actions editor needs to include a custom action in the Install folder. The CustomActionData property for this custom action can read the environment value from the user interface and pass the values to the install program as follows:
/ENVT=[BUTTON3]
/sectionNames=”nameOfSection1,nameOfSection2,etc…”
/provName=”configProtectedDataProviderNameInConfigFile”
/encryptApplicationSettings=”True|False”
For example:
/ENVT=[BUTTON3] /sectionNames=”securityConfiguration ”
/provName=”DPAPIProtection” /encryptApplicationSettings=”True”
-
Don’t forget to include the installer project output to the setup project (in the File System Editor, like other project outputs).
-
The app.config includes the DPAPIProtection as follows:
<configProtectedData defaultProvider=“DPAPIProtection“>
<providers>
<add useMachineProtection=“true“
name=“DPAPIProtection“
type=“System.Configuration.DpapiProtectedConfigurationProvider,
System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a,
processorArchitecture=MSIL“ />
</providers>
</configProtectedData>
There are some nice references out there. I’d like to recommend these two. They were a great help to me and I definitely wanted to acknowledge them: