BizTalk Deployment Framework is one of those pearls for BizTalk developers, allowing complex BizTalk solutions to be deployed easily, having all our artifacts and dependencies together in one MSI. To make life even better for us, we can also automate the process of building and deploying these BTDF MSI's by using PowerShell. This especially comes in handy once we start having large projects with many BizTalk applications, where we would have to spend a lot of time manually running all these MSI's.
Using PowerShell we will make scripts which will handle all steps of the build and deployment process for us. This will make sure our applications are always deployed in the correct order, using the right versions, and with minimal effort. We have some general helper functions, which will help us clear log files, wait for user input, iterate through directories, etc. We assume you have are using some of the BTDF best practices for these scripts, where it comes to naming conventions and folder structure. Of course, in case anything differs in your environment, you can easily adjust the scripts to meet your requirements.
We will first create the PowerShell scripts which will help us build our applications. To be able to share these scripts along your different developers, where there might be some differences in the environments in how directories are set up, we will make use of a small csv file to hold our build environment settings.
Name;Value
projectsBaseDirectory;F:\tfs
installersOutputDirectory;F:\Deployment
visualStudioDirectory;F:\Program Files (x86)\Microsoft Visual Studio 11.0
We will load these settings in our script and assign them to specific parameters.
$settings = Import-Csv Settings_BuildEnvironment.csv
foreach($setting in $settings)
{
# The directory where the BizTalk projects are stored
if($setting.'Name;Value'.Split(";")[0].Trim() -eq "projectsBaseDirectory") { $projectsBaseDirectory = $setting.'Name;Value'.Split(";")[1].Trim() }
# The directory where the MSI's should be saved to
if($setting.'Name;Value'.Split(";")[0].Trim() -eq "installersOutputDirectory") { $installersOutputDirectory = $setting.'Name;Value'.Split(";")[1].Trim() }
# Directory where Visual Studio resides
if($setting.'Name;Value'.Split(";")[0].Trim() -eq "visualStudioDirectory") { $visualStudioDirectory = $setting.'Name;Value'.Split(";")[1].Trim() }
}
Now that we have our environment specific parameters set, we can create a function which will build our BizTalk application. We will assume you have several projects, which are in folders under a common directory ($projectsBaseDirectory), which is probably your source control root directory. Your application's directories should be under these project's directories. We will building the application by calling Visual Studio, and using the log to check if the build was successful.
function BuildBizTalkApplication([string]$application, [string]$project)
{
# Set directory where the BizTalk projects for the current project are stored
$projectsDirectory = "$projectsBaseDirectory\$project"
# Clear log files and old installers
ClearLogFiles $application
# Build application
Write-Host "Building $application" -ForegroundColor Cyan
$exitCode = (Start-Process -FilePath "$visualStudioDirectory\Common7\IDE\devenv.exe" -ArgumentList """$projectsDirectory\$application\$application.sln"" /Build Release /Out $application.log" -PassThru -Wait).ExitCode
# Check result
if($exitCode -eq 0 -and (Select-String -Path "$application.log" -Pattern "0 failed" -Quiet) -eq "true")
{
Write-Host "$application built succesfully" -ForegroundColor Green
}
else
{
Write-Host "$application not built succesfully" -ForegroundColor Red
WaitForKeyPress
}
}
Once the applications are built, we will also need to create MSI's for them, which is where the BTDF comes in. This can be done by calling MSBuild, and passing in the .btdfproj file. Finally we copy the MSI to a folder, so all our MSI's are together in one location and from there can be copied to the BizTalk server.
function BuildBizTalkMsi([string]$application, [string]$project)
{
# Set directory where the BizTalk projects for the current project are stored
$projectsDirectory = "$projectsBaseDirectory\$project"
# Build installer
$exitCode = (Start-Process -FilePath """$env:windir\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe""" -ArgumentList "/t:Installer /p:Configuration=Release ""$projectsDirectory\$application\Deployment\Deployment.btdfproj"" /l:FileLogger,Microsoft.Build.Engine;logfile=$application.msi.log" -PassThru -Wait).ExitCode
# Check result
if($exitCode -eq 0)
{
Write-Host "MSI for $application built succesfully" -ForegroundColor Green
}
else
{
Write-Host "MSI for $application not built succesfully" -ForegroundColor Red
WaitForKeyPress
}
# Copy installer
copy "$projectsDirectory\$application\Deployment\bin\Release\*.msi" "$installersOutputDirectory"
}
Once the MSI's have been created we can copy them to our BizTalk server, and start the deployment process. This process consists of 4 steps, starting with undeploying the old applications, uninstalling the old MSI's, installing the new MSI's and deploying the new applications. If your applications have dependencies on other applications, it's also important to undeploy and deploy them in the correct order. We will want to use one set of scripts for all our OTAP environments, so we will be using another csv file here to keep track of the environment specific settings, like directories and config files to use.
We will start by loading the environment specific parameters.
$settings = Import-Csv Settings_DeploymentEnvironment.csv
foreach($setting in $settings)
{
# Program Files directory where application should be installed
if($setting.'Name;Value'.Split(";")[0].Trim() -eq "programFilesDirectory") { $programFilesDirectory = $setting.'Name;Value'.Split(";")[1].Trim() }
# Suffix as set in in the ProductName section of the BTDF project file. By default this is " for BizTalk".
if($setting.'Name;Value'.Split(";")[0].Trim() -eq "productNameSuffix") { $productNameSuffix = $setting.'Name;Value'.Split(";")[1].TrimEnd() }
# Indicator if we should deploy to the BizTalkMgmtDB database from this server. In multi-server environments this should be true on 1 server, and false on the others
if($setting.'Name;Value'.Split(";")[0].Trim() -eq "deployBizTalkMgmtDB") { $deployBizTalkMgmtDB = $setting.'Name;Value'.Split(";")[1].Trim() }
}
Now we can write our function for undeploying. We will also be using MSBuild in conjuntion with BTDF here, by passing in the .btdfproj file location with the Undeploy switch. To do so, we will call the following function for each application to be undeployed. Remember to do the undeployment in the correct order.
function UndeployBizTalkApplication([string]$application, [string]$version)
{
# Execute undeployment
$exitCode = (Start-Process -FilePath "$env:windir\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe" -ArgumentList """$programFilesDirectory\$application$productNameSuffix\$version\Deployment\Deployment.btdfproj"" /t:Undeploy /p:DeployBizTalkMgmtDB=$deployBizTalkMgmtDB /p:Configuration=Server" -Wait -Passthru).ExitCode
if($exitCode -eq 0)
{
Write-Host "$application undeployed successfully" -ForegroundColor Green
}
else
{
Write-Host "$application not undeployed successfully" -ForegroundColor Red
}
}
Once all the applications for our project have been undeployed, we will uninstall the old MSI's. To do this, we will iterate through the MSI's in the specified directory, where we will pass in the directory with the last used installers.
function UninstallBizTalkApplications($msiDirectory)
{
# Get MSI's to be installed
$files = GetMsiFiles $msiDirectory
# Loop through MSI files
foreach($file in $files)
{
UninstallBizTalkApplication $file
}
}
This will call the uninstall command. We will assume our MSI's are named according to BTDF defaults, which is applicationname-version, so for example MyApplication-1.0.0.msi.
function UninstallBizTalkApplication([System.IO.FileInfo]$fileInfo)
{
# Get application name
$applicationName = $fileInfo.BaseName.Split("-")[0]
# Set installer path
$msiPath = $fileInfo.FullName
# Uninstall application
$exitCode = (Start-Process -FilePath "msiexec.exe" -ArgumentList "/x ""$msiPath"" /qn" -Wait -Passthru).ExitCode
# Check if uninstalling was successful
if($exitCode -eq 0)
{
Write-Host "$applicationName uninstalled successfully" -ForegroundColor Green
}
else
{
Write-Host "$applicationName not uninstalled successfully" -ForegroundColor Red
}
}
The next step will be to install all the new MSI's we have just built. Here we will once again iterate through the specified directory, where we will now pass in the directory with the new installers.
function InstallBizTalkApplications([string]$msiDirectory)
{
# Clear log files
ClearLogFiles $msiDirectory
# Get MSI's to be installed
$files = GetMsiFiles $msiDirectory
# Loop through MSI files
foreach($file in $files)
{
# Install application
InstallBizTalkApplication $file
}
}
We will also have to load the environment specific parameters here.
$settings = Import-Csv Settings_DeploymentEnvironment.csv
foreach($setting in $settings)
{
# Program Files directory where application should be installed
if($setting.'Name;Value'.Split(";")[0].Trim() -eq "programFilesDirectory") { $programFilesDirectory = $setting.'Name;Value'.Split(";")[1].Trim() }
# Suffix as set in in the ProductName section of the BTDF project file. By default this is " for BizTalk".
if($setting.'Name;Value'.Split(";")[0].Trim() -eq "productNameSuffix") { $productNameSuffix = $setting.'Name;Value'.Split(";")[1].TrimEnd() }
}
And now we can install the MSI. As mentioned before, we will assume our MSI's are named according to BTDF defaults (applicationname-version.msi).
function InstallBizTalkApplication([System.IO.FileInfo]$fileInfo)
{
# Get application name and version
# We assume msi file name is in the format ApplicationName-Version
$application = $fileInfo.BaseName.Split("-")[0]
$version = $fileInfo.BaseName.Split("-")[1]
# Directory where MSI resides
$msiDirectory = $fileInfo.Directory
# Set log name
$logFileName = "$msiDirectory\$application.log"
# Set installer path
$msiPath = $fileInfo.FullName
# Install application
Start-Process -FilePath "msiexec.exe" -ArgumentList "/i ""$msiPath"" /passive /log ""$logFileName"" INSTALLDIR=""$programFilesDirectory\$application$productNameSuffix\$version""" -Wait -Passthru | Out-Null
# Check if installation was successful
if((Select-String -Path $logFileName -Pattern "success or error status: 0" -Quiet) -eq "true")
{
Write-Host "$application installed successfully" -ForegroundColor Green
}
else
{
Write-Host "$application not installed successfully" -ForegroundColor Red
}
}
The last step is to deploy the applications we just installed. First we again have to load the environment specific parameters.
$settings = Import-Csv Settings_DeploymentEnvironment.csv
foreach($setting in $settings)
{
# Program Files directory where application should be installed
if($setting.'Name;Value'.Split(";")[0].Trim() -eq "programFilesDirectory") { $programFilesDirectory = $setting.'Name;Value'.Split(";")[1].Trim() }
# Suffix as set in in the ProductName section of the BTDF project file. By default this is " for BizTalk".
if($setting.'Name;Value'.Split(";")[0].Trim() -eq "productNameSuffix") { $productNameSuffix = $setting.'Name;Value'.Split(";")[1].TrimEnd() }
# Indicator if we should deploy to the BizTalkMgmtDB database from this server. In multi-server environments this should be true on 1 server, and false on the others
if($setting.'Name;Value'.Split(";")[0].Trim() -eq "deployBizTalkMgmtDB") { $deployBizTalkMgmtDB = $setting.'Name;Value'.Split(";")[1].Trim() }
# Name of the BTDF environment settings file for this environment.
if($setting.'Name;Value'.Split(";")[0].Trim() -eq "environmentSettingsFileName") { $environmentSettingsFileName = $setting.'Name;Value'.Split(";")[1].Trim() }
}
Deploying is also done by using MSBuild with BTDF, by specifying the Deploy flag. For this we will be calling the following function for each application to be deployed, which of course should be done in the correct order.
function DeployBizTalkApplication([string]$application, [string]$version)
{
# Set log file
$logFileName = "$programFilesDirectory\$application$productNameSuffix\$version\DeployResults\DeployResults.txt"
# Execute deployment
$exitCode = (Start-Process -FilePath "$env:windir\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe" -ArgumentList "/p:DeployBizTalkMgmtDB=$deployBizTalkMgmtDB;Configuration=Server;SkipUndeploy=true /target:Deploy /l:FileLogger,Microsoft.Build.Engine;logfile=""$programFilesDirectory\$application$productNameSuffix\$version\DeployResults\DeployResults.txt"" ""$programFilesDirectory\$application$productNameSuffix\$version\Deployment\Deployment.btdfproj"" /p:ENV_SETTINGS=""$programFilesDirectory\$application$productNameSuffix\$version\Deployment\EnvironmentSettings\$environmentSettingsFileName.xml""" -Wait -Passthru).ExitCode
# Check if deployment was successful
if($exitCode -eq 0 -and (Select-String -Path $logFileName -Pattern "0 Error(s)" -Quiet) -eq "true")
{
Write-Host "$application deployed successfully" -ForegroundColor Green
}
else
{
Write-Host "$application not deployed successfully" -ForegroundColor Red
}
}
From the same location where we call this function, we will also do some additional checks. Sometimes you will want to import some registry files or execute a SQL script, which you might not want to include in your BTDF MSI for any reason. Also, once everything has been deployed, you might want to restart your host instances and IIS, which can also be handled here.
function DeployBizTalkApplications([string[]]$applicationsInOrderOfDeployment, [string[]]$versions, [string]$scriptsDirectory)
{
# Check which restarts should be done
$resetIIS = CheckIfIISShouldBeReset
$restartHostInstances = CheckIfHostinstancesShouldBeRestarted
# Loop through applications to be deployed
for($index = 0; $index -lt $applicationsInOrderOfDeployment.Length; $index++)
{
# Deploy application
DeployBizTalkApplication $applicationsInOrderOfDeployment[$index] $versions[$index]
}
# Get SQL files to be executed
$sqlFiles = GetSQLFiles $scriptsDirectory
# Loop through SQL files
foreach($sqlFile in $sqlFiles)
{
# Execute SQL file
ExecuteSqlFile $sqlFile
}
# Get registry files to be imported
$registryFiles = GetRegistryFiles $scriptsDirectory
# Loop through registry files
foreach($registryFile in $registryFiles)
{
# Import registry file
ImportRegistryFile $registryFile
}
# Do restarts
if($resetIIS)
{
DoIISReset
}
if($restartHostInstances)
{
DoHostInstancesRestart
}
}
Finally, we have to stitch it all together. When you have downloaded the complete set of functions from this article, you can specify your build scripts as following, where you will only have to change the project name and applications to be built.
# Project specific settings
$projectName = "OrderSystem"
$applications = @("Contoso.OrderSystem.Orders", "Contoso.OrderSystem.Invoices", "Contoso.OrderSystem.Payments")
# Import custom functions
. .\Functions_Build.ps1
# Build the applications
BuildAndCreateBizTalkInstallers $applications $projectName
# Wait for user to exit
WaitForKeyPress
As for deployment, all those steps can also be called from one single script as following. Once again, the only thing to change is the project specific settings.
# Project specific settings
$oldInstallersDirectory = "F:\tmp\R9"
$newInstallersDirectory = "F:\tmp\R10"
$newApplications = @("Contoso.OrderSystem.Orders", "Contoso.OrderSystem.Invoices", "Contoso.OrderSystem.Payments")
$oldApplications = @("Contoso.OrderSystem.Payments", "Contoso.OrderSystem.Invoices", "Contoso.OrderSystem.Orders")
$oldVersions = @("1.0.0", "1.0.0", "1.0.0")
$newVersions = @("1.0.0", "1.0.1", "1.0.0")
# Import custom functions
. .\Functions_Deploy.ps1
. .\Functions_Undeploy.ps1
. .\Functions_Install.ps1
. .\Functions_Uninstall.ps1
# Undeploy the applications
UndeployBizTalkApplications $oldApplications $oldVersions
# Wait for user to continue
WaitForKeyPress
# Uninstall the applications
UninstallBizTalkApplications $oldInstallersDirectory
# Wait for user to continue
WaitForKeyPress
# Install the applications
InstallBizTalkApplications $newInstallersDirectory
# Wait for user to continue
WaitForKeyPress
# Deploy the applications
DeployBizTalkApplications $newApplications $newVersions $newInstallersDirectory
# Wait for user to exit
WaitForKeyPress
As you can see, using these PowerShell scripts you can setup scripts for your build and deployment processes very quickly. And by automating all these steps, we will have to spend much less time on builds and deployments, as we will only have to start our scripts, and the rest just goes automatically.