Handling per AppDomain Configuration File with PowerShell

In BizTalk, it is not unusual to have to store application-specific parameters as runtime configuration. On way to do this is to modify the BizTalk btsntsvc.exe.config configuration file.

Unfortunately, this has severe drawbacks, since a single mistake in the configuration file (such as a typo) will prevent BizTalk from running at all. Also, this does not lend itself well to the separation of concern that the logical concept of "Application" gives us in the first place. In my opinion, it is much safer and maintainable to separate parameters belonging each BizTalk applications into a different configuration file.

Fortunately, the BizTalk Orchestration Engine supports this feature around the concept of Application Domain. As the documentation states, assemblies that comprise our BizTalk application are assigned to an Application Domain thanks to assignment rules stored inside the BizTalk configuration file.

However, modifying the configuration file is itself a tedious task and is also prone to errors. Especially during development, when the separation of our solution into discrete Applications is not yet cast in stone and must be adapted somewhat frequently.

In order to ease this task, as well as provide a streamline approach to handling the association between a BizTalk application and its separate configuration file, I wrote a couple of PowerShell scripts that allow to create, update or remove an association between a BizTalk application (more specifically, its Application Domain) and a dedicated configuration file.

Specifying a configuration file for a BizTalk application

The following picture illustrates the contents of a plain old vanilla btsntsvc.exe.config file:

The following command in PowerShell creates a new association between assemblies from a BizTalk application and a specified configuration file.

PS:\> New-BizTalkConfiguration Application -ConfigFile C:\Application.config
PS:\>

Here is the contents of the resulting btsntsvc.exe.config.

Removing the association between a BizTalk application and its configuration file

The following command removes an association between a BizTalk application and its configuration file. In particular, this command cleans the BizTalk configuration file and ensures that no empty tags are left behind.

PS:\> Remove-BizTalkConfiguration Application
PS:\>

After running this command, the BizTalk configuration file, illustrated above, is restored to its original contents.

PowerShell modules for simplified maintenance and script cohesion

For ease of maintainance and increase script cohesion, the previous scripts referred to above are part of a PowerShell module. A PowerShell module allows to …

Our module, called for instance, BizTalkFactory.Management, comprises the scripts necessary to handle various tasks related to deploying and managing solutions around BizTalk. In this module, the two Cmdlets New-BizTalkConfiguration and Remove-BizTalkConfiguration are the subject of this post. Those scripts, in turn, are implemented thanks to a comprehensive core script-CmdLet that handles updates to the BizTalk configuration file:

Function Handle-BizTalkConfiguration
{
	[CmdletBinding()]
	param (
		[Parameter(Position = 0, Mandatory = $true)]
		[System.String]
		$appDomainSpecName,

		[Alias("AssemblyNamePattern")]
		[System.String]
		$pattern = "$($appDomainSpecName).*",
			
		[Alias("ConfigFile")]
		[System.String]
		$path,
		
		[Alias("Remove")]
		[Switch]
		$uninstall = $false
	)

	BEGIN
	{
		## A private helper function to make it easy
		## to conditionnaly create an XML child element
		
		Function Append-XmlElement
		{
			[CmdletBinding()]
			param (
				[Parameter(Mandatory = $true)]
				[Alias("Name")]
				[String]$elementName,
				[Parameter(Mandatory = $true)]
				[Alias("Document")]
				[System.Xml.XmlDocument]$xmlDocument,
				[Parameter(Mandatory = $true)]
				[Alias("Node")]
				[System.Xml.XmlNode]$xmlNode
			)
			
			[System.Xml.XmlNode] $xmlElement = $xmlNode.SelectSingleNode($elementName)
			
			if (-not $xmlElement)
			{
				$xmlElement = $xmlDocument.CreateElement($elementName)
				$xmlElement = $xmlNode.AppendChild($xmlElement)
			}
			
			return $xmlElement
		}
		
		## Validate parameters
		
		if (-not $appDomainSpecName.EndsWith("AppDomain"))
			{ $appDomainSpecName = "$($appDomainSpecName)AppDomain" }

		if ($path)
		{
			if (-not (Test-Path $path)) { throw "the specified configuration file does not exists" }
			$path = (Resolve-Path $path).ProviderPath
		}

		## Initialize some variables

		$bts_registry = "HKLM:\SOFTWARE\Microsoft\BizTalk Server\3.0"
		$bts_installpath = (Get-ItemProperty $bts_registry -Name InstallPath).InstallPath
		$bts_ntsvcconfig = (Join-Path $bts_installpath "BTSNTSvc.exe.config")
		
		$bts_xprocsection = "Microsoft.XLANGs.BizTalk.CrossProcess.XmlSerializationConfigurationSectionHandler"
		$bts_xprocassembly = "Microsoft.XLANGs.BizTalk.CrossProcess"
		
		if (-not (Test-Path $bts_ntsvcconfig)) { throw "missing required $bts_ntsvcconfig file" }
	}

	PROCESS
	{
		## Load the XML BTSNTSvc.exe.config file

		[System.Xml.XmlDocument] $xml = (Get-Content $bts_ntsvcconfig)
		[System.Xml.XmlNode] $config = $xml.configuration
		
		## Add configSections section (if necessary)
		
		[System.Xml.XmlNode] $configSections = Append-XmlElement "configSections" -Document $xml -Node $config
		
		if (-not $configSections.SelectSingleNode("section[@name='xlangs']"))
		{
			[System.Xml.XmlNode] $xlangs = `
				Append-XmlElement "xlangs" `
					-Document $xml `
					-Node $configSections

			$xlangs.SetAttribute("name", "xlangs")
			$xlangs.SetAttribute("type", "$($bts_xprocsection),$($bts_xprocassembly)")	
		}
		
		## Add xlangs section (if necessary)
			
		[System.Xml.XmlNode] $xlangs = `
			Append-XmlElement "xlangs" `
				-Document $xml `
				-Node $config

		[System.Xml.XmlNode] $configuration = `
			Append-XmlElement "Configuration" `
				-Document $xml `
				-Node $xlangs
		
		[System.Xml.XmlNode] $appDomains = `
			Append-XmlElement "AppDomains" `
				-Document $xml `
				-Node $configuration
		
		if (-not $appDomains.HasAttribute("AssembliesPerDomain"))
			{ $appDomains.SetAttribute("AssembliesPerDomain", "10") }

		[System.Xml.XmlNode] $defaultSpec = `
			Append-XmlElement "DefaultSpec" `
				-Document $xml `
				-Node $appDomains

		if (-not $defaultSpec.HasAttribute("SecondsIdleBeforeShutdown"))
			{ $defaultSpec.SetAttribute("SecondsIdleBeforeShutdown", "1200") }
		if (-not $defaultSpec.HasAttribute("SecondsEmptyBeforeShutdown"))
			{ $defaultSpec.SetAttribute("SecondsEmptyBeforeShutdown", "1800") }
				
		[System.Xml.XmlNode] $appDomainSpecs = `
			Append-XmlElement "AppDomainSpecs" `
				-Document $xml `
				-Node $appDomains
			
		[System.Xml.XmlNode] $patternAssignmentRules = `
			Append-XmlElement "PatternAssignmentRules" `
				-Document $xml `
				-Node $appDomains
		
		[System.Xml.XmlNode] $appDomainSpec = `
			$appDomainSpecs.SelectSingleNode("AppDomainSpec[@Name='$($appDomainSpecName)']")
		[System.Xml.XmlNode] $patternAssignmentRule = `
			$patternAssignmentRules.SelectSingleNode("PatternAssignmentRule[@AppDomainName='$($appDomainSpecName)']")
		
		## Install new AppDomainSpec
		
		if ((-not $appDomainSpec) -and (-not $uninstall))
		{
			$appDomainSpec = `
				Append-XmlElement "AppDomainSpec" `
					-Document $xml `
					-Node $appDomainSpecs
					
			$appDomainSpec.SetAttribute("Name", $appDomainSpecName)

			$patternAssignmentRule = `
				Append-XmlElement "PatternAssignmentRule" `
					-Document $xml `
					-Node $patternAssignmentRules
					
			$patternAssignmentRule.SetAttribute("AppDomainName", $appDomainSpecName)
		}
		
		## Update existing AppDomainSpec
		
		if ($appDomainSpec -and (-not $uninstall))
		{
			[System.Xml.XmlNode] $baseSetup = `
				Append-XmlElement "BaseSetup" `
					-Document $xml `
					-Node $appDomainSpec
					
			[System.Xml.XmlNode] $configurationFile = `
				Append-XmlElement "ConfigurationFile" `
					-Document $xml `
					-Node $baseSetup
		
			$configurationFile.set_InnerText($path)
			$patternAssignmentRule.SetAttribute("AssemblyNamePattern", $pattern)
		}
		
		## Remove existing AppDomainSpec
		
		if ($appDomainSpec -and $uninstall)
		{
			$appDomainSpec = $appDomainSpecs.RemoveChild($appDomainSpec)
			$patternAssignmentRule = $patternAssignmentRules.RemoveChild($patternAssignmentRule)	
			
			if ($appDomainSpecs.ChildNodes.Count -eq 0)
			{
				$appDomainSpecs = $appDomains.RemoveChild($appDomainSpecs)
				$patternAssignmentRules = $appDomains.RemoveChild($patternAssignmentRules)
			}
		}

		## Save changes
		
		$xml.Save($bts_ntsvcconfig)
	}

	END
	{
	}
}

A PowerShell module is described by its module manifest. This is a text file that contains information about the module itself and the various scripts available as part of the module. A module manifest can be created very easily from a PowerShell session via the CmdLet. Module manifest bear the .psd1 file extension.

The module itself is a folder, whose name corresponds to the module name, stored under the WindowsPowerShell directory alongside the user profile. This folder contains the manifest, as well as the scripts available, in the form of files bearing the .psm1 extension.

In order to use the scripts, the PowerShell module must first be imported in the current session. This can be done when required, or put inside the PowerShell profile in order to be made available for each subsequent sessions.

PS:\> Import-Module BizTalkFactory.Management
PS:\>

Source Code for the PowerShell Module

For completeness, here is the code for the module manifest and the two other relevant CmdLets:

#
# Module manifest for module 'BizTalkFactory.Management'
#
# Generated by: Maxime Labelle
#
# Generated on: 18/12/2009
#

@{

# These modules will be processed when the module manifest is loaded.
NestedModules = @( `
	  "New-BizTalkConfiguration.psm1"
	, "Remove-BizTalkConfiguration.psm1"
	, "Handle-BizTalkConfiguration.psm1"
)

# This GUID is used to uniquely identify this module.
GUID = '26ee8dd3-0591-44ed-a168-4cfb320ff2b5'

# The author of this module.
Author = 'Maxime Labelle'

# The company or vendor for this module.
CompanyName = 'Logica Management Consulting'

# The copyright statement for this module.
Copyright = 'Copyright Maxime Labelle 2009.'

# The version of this module.
ModuleVersion = '1.0'

# A description of this module.
Description = 'BizTalk Factory PowerShell Scripts'

# The minimum version of PowerShell needed to use this module.
PowerShellVersion = '2.0'

# The CLR version required to use this module.
CLRVersion = '2.0'

# Functions to export from this manifest.
ExportedFunctions = '*'

# Aliases to export from this manifest.
ExportedAliases = '*'

# Variables to export from this manifest.
ExportedVariables = '*'

# Cmdlets to export from this manifest.
ExportedCmdlets = '*'

# This is a list of other modules that must be loaded before this module.
RequiredModules = @()

# The script files (.ps1) that are loaded before this module.
ScriptsToProcess = @()

# The type files (.ps1xml) loaded by this module.
TypesToProcess = @()

# The format files (.ps1xml) loaded by this module.
FormatsToProcess = @()

# A list of assemblies that must be loaded before this module can work.
RequiredAssemblies = @()

# Lists additional items like icons, etc. that the module will use.
OtherItems = @()

# Module specific private data can be passed via this member.
PrivateData = ''

}
Function New-BizTalkConfiguration
{	
	[CmdletBinding()]
	param (
		[Parameter(Position = 0, Mandatory = $true)]
		[String]
		$appDomainSpecName,
			
		[Alias(""ConfigFile"")]
		[Parameter(Mandatory = $true)]
		[String]
		$path,

		[Alias("AssemblyNamePattern")]
		[String]
		$pattern = "$($appDomainSpecName).*"
	)

	Handle-BizTalkConfiguration $appDomainSpecName  `
		-Path $path `
		-Pattern $pattern
}
Function Remove-BizTalkConfiguration
{	
	[CmdletBinding()]
	param (
		[Parameter(Position = 0, Mandatory = $true)]
		[String]
		$appDomainSpecName
	)

	Handle-BizTalkConfiguration $appDomainSpecName -Remove
}

You can download the source for this module here.

This entry was posted in PowerShell. Bookmark the permalink.