Search class based data structure like $xml.SelectNodes() - powershell

I am refactoring some functional code to use classes, and trying to understand the "best" (most performant, easiest to read, least likely to be deprecated, etc.) way to do so.
The functional code uses XML as a data structure, and I need to be able to search for nodes based on certain criteria. So, in simplified form...
$xml = [XML]#"
<Definitions>
<Package id="A">
<Task id="A.1">
<DisplayName>Ignore</DisplayName>
</Task>
<Task id="A.2">
<DisplayName>Find</DisplayName>
</Task>
</Package>
<Package id="B">
<Task id="B.1">
<DisplayName>Ignore</DisplayName>
</Task>
<Task id="B.2">
<DisplayName>Find</DisplayName>
</Task>
<Task id="B.3">
<DisplayName>Ignore</DisplayName>
</Task>
</Package>
<Package id="C">
<Task id="C.1">
<DisplayName>Ignore</DisplayName>
</Task>
<Task id="C.2">
<DisplayName>Ignore</DisplayName>
</Task>
<Task id="C.3">
<DisplayName>Find</DisplayName>
</Task>
</Package>
</Definitions>
"#
$target = 'Find'
$finds = $xml.SelectNodes("//Task/DisplayName[.='$target']")
foreach ($find in $finds) {
Write-Host "$($find.ParentNode.id)"
}
I have managed to get Package and Task collections working, and I can populate a collection of Packages, containing collections of Tasks, based on the same XML, like so...
class Package {
# Properties
[String]$ID
[System.Collections.Generic.List[object]]$Tasks = [System.Collections.Generic.List[object]]::New()
# Constructor
Package ([String]$id) {
$this.ID = $id
}
# Method
[Void] addTask([Task]$newTask) {
$this.Tasks.Add($newTask)
}
}
class Task {
# Properties
[String]$ID
[String]$DisplayName
# Constructor
Task ([String]$id) {
$this.ID = $id
}
}
$definitions = [System.Collections.Generic.List[object]]::New()
foreach ($package in $xml.SelectNodes("//Package")) {
$newPackage = [Package]::New($package.ID)
foreach ($task in $package.SelectNodes("Task")) {
$newTask = [Task]::New($task.ID)
$newTask.DisplayName = $task.DisplayName
$newPackage.addTask($newTask)
}
$definitions.Add($newPackage)
}
And I can verify that is working with some foreach loops...
foreach ($package in $definitions) {
Write-Host "$($package.ID)"
foreach ($task in $package.Tasks) {
Write-Host " $($task.ID) $($task.DisplayName)"
}
}
Now I want to replicate $finds = $xml.SelectNodes("//Task/DisplayName[.='$target']"), and do it the "right" way. I could just iterate through the list like this.
foreach ($package in $definitions) {
foreach ($task in $package.Tasks) {
if ($task.DisplayName -eq $target) {
Write-Host "$($task.ID)"
}
}
}
Or I could have a Find method in the Package class that takes $target as an argument, and iterates over it's own Tasks.
But I wonder if there is some Automatic Variable that already contains all objects of a particular Type, or a way to populate a variable with all objects of a particular Type, so I am iterating over a smaller list. But then I need to be able to find the Parent, and at that point this whole line of thinking seems to break down, since there is no Parent data unless I provide it.
So, what is the best way to do this search?
Also, FWIW, the reason for this exercise is that fact that there are actually about 30 different variations of Task that I will need to implement, with LOTS of shared behavior. Doing that in Functions has led to a bunch of redundant code and lots of work implementing new tasks or fixing bugs in the duplicated code. Inheritance will fix that, and a bunch of other issues that have come up, so moving to classes makes a lot of sense in the bigger picture.

As Theo alludes to in the comments, for a simple object hierarchy like your Package, you can just use Where-Object:
$package.Tasks |Where-Object { $_.DisplayName -eq $target }
Implementing a FindTasks() method is therefore just a question of wrapping Where-Object:
class Package
{
# ...
[Task[]]
FindTasks([scriptblock]$filter)
{
return $this.Tasks |Where-Object $filter
}
}
After which the user can do $package.FindTasks({$_.ID -eq $ID -or $_.DisplayName -like '*keyword*'}) or whatever else they want to filter on

Related

PowerShell classes and .NET events

I am working on a PowerShell script with a small WPF GUI. My code is organzized in a class from which a singleton is created. I have read that $this inside an event handler script block points to the event sender and not to my containing class instance. How can I access my class instance from the event handler?
Ex.
class MyClass {
$form #Reference to the WPF form
[void] StartAction([object] $sender, [System.Windows.RoutedEventArgs] $e) {
...
}
[void] SetupEventHandlers() {
$this.form.FindName("BtnStartAction").add_Click({
param($sender, $e)
# !!!! Does not work, $this is not my class instance but the event sender !!!!
$this.StartAction($sender, $e)
})
}
[void] Run() {
$this.InitWpf() #Initializes the form from a XAML file.
$this.SetupEventHandlers()
...
}
}
$instance = [MyClass]::new()
$instance.Run()
Indeed, the automatic $this variable in a script block acting as a .NET event handler refers to the event sender.
If an event-handler script block is set up from inside a method of a PowerShell custom class, the event-sender definition of $this shadows the usual definition in a class method (referring to the class instance at hand).
There are two workarounds, both relying on PowerShell's dynamic scoping, which allows descendant scopes to see variables from ancestral scopes.
Use Get-Variable -Scope 1 to reference the parent scope's $this value (event-handler script blocks run in a child scope of the caller).
[void] SetupEventHandlers() {
$this.form.FindName("BtnStartAction").add_Click({
param($sender, $e)
# Get the value of $this from the parent scope.
(Get-Variable -ValueOnly -Scope 1 this).StartAction($sender, $e)
})
}
Taking more direct advantage of dynamic scoping, you can go with Abdul Niyas P M's suggestion, namely to define a helper variable in the caller's scope that references the custom-class instance under a different name, which you can reference in - potentially multiple - event-handler script blocks set up from the same method:
Note that a call to .GetNewClosure() is required on the script block, so as to make the helper variable available inside the script block.Tip of the hat to Sven.
[void] SetupEventHandlers() {
# Set up a helper variable that points to $this
# under a different name.
$thisClassInstance = $this
# Note the .GetNewClosure() call.
$this.form.FindName("BtnStartAction").add_Click({
param($sender, $e)
# Reference the helper variable.
$thisClassInstance.StartAction($sender, $e)
}.GetNewClosure())
}
Also note that, as of PowerShell 7.2, you cannot directly use custom-class methods as event handlers - this answer shows workarounds, which also require solving the $this shadowing problem.
Self-contained sample code:
Important: Before running the code below, ensure that you have run the following in your session:
# Load WPF assemblies.
Add-Type -AssemblyName PresentationCore, PresentationFramework
Unfortunately, placing this call inside a script that contains the code below does not work, because class definitions are processed at parse time, i.e., before the Add-Type command runs, whereas all .NET types referenced by a class must already be
loaded - see this answer.
While using assembly statements in lieu of Add-Type calls may some day be a solution (they too are processed at parse time), their types aren't currently discovered until runtime, leading to the same problem - see GitHub issue #3641; as a secondary problem, well-known assemblies cannot currently be referenced by file name only; e.g., using assembly PresentationCore.dll does not work, unlike Add-Type -AssemblyName PresentationCore - see GitHub issue #11856
# IMPORTANT:
# Be sure that you've run the following to load the WPF assemblies
# BEFORE calling this script:
# Add-Type -AssemblyName PresentationCore, PresentationFramework
class MyClass {
$form #Reference to the WPF form
[void] InitWpf() {
[xml] $xaml=#"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Test"
Title="MainWindow" Height="500" Width="500">
<Grid>
<Button x:Name="BtnStartAction" Content="StartAction" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" IsDefault="True" Height="22" Margin="170,0,0,0" />
<TextBox x:Name="Log" Height="400" TextWrapping="Wrap" VerticalAlignment="Top" AcceptsReturn="True" AcceptsTab="True" Padding="4" VerticalScrollBarVisibility="Auto" Margin="0,40,0,0"/>
</Grid>
</Window>
"#
$this.form = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))
}
[void] StartAction([object] $sender, [System.Windows.RoutedEventArgs] $e) {
$tb = $this.form.FindName("Log")
$tb.Text += $e | Out-String
}
[void] SetupEventHandlers() {
$btn = $this.form.FindName("BtnStartAction")
# -- Solution with Get-Variable
$btn.add_Click({
param($sender, $e)
(Get-Variable -ValueOnly -Scope 1 this).StartAction($sender, $e)
}.GetNewClosure())
# -- Solution with helper variable.
# Note the need for .GetNewClosure()
# Helper variable that points to $this under a different name.
$thisClassInstance = $this
$btn.add_Click({
param($sender, $e)
$thisClassInstance.StartAction($sender, $e)
}.GetNewClosure())
}
[void] Run() {
$this.InitWpf() #Initializes the form from a XAML file.
$this.SetupEventHandlers()
$this.form.ShowDialog()
}
}
$instance = [MyClass]::new()
$instance.Run()
The above demonstrates both workarounds: when you press the StartAction button, both event handlers should add the event-arguments object they've each received to the text box, as shown in the following screenshot:
Try this sample
class MyClass {
$form #Reference to the WPF form
[void] StartAction([System.Windows.RoutedEventArgs] $e) {
#$this ...
}
[void] SetupEventHandlers() {
$this.form.FindName("BtnStartAction").add_Click({
param($sender, $e)
([MyClass]$sender).StartAction($e)
})
}
[void] Run() {
$this.InitWpf()
$this.SetupEventHandlers()
}
}
$instance = [MyClass]::new()
$instance.Run()
Edit 1:
Could you try creating a delegate referring to a dedicated method of the class like this?
class MyClass {
$form #Reference to the WPF form
[void] StartAction() {
#$this ...
}
[void] SetupEventHandlers() {
$handler = [System.EventHandler]::CreateDelegate([System.EventHandler], $this, "Handler")
$this.form.FindName("BtnStartAction").add_Click($handler)
}
[void] Handler ([System.Object]$sender, [System.EventArgs]$e) {
$this.StartAction()
}
[void] Run() {
$this.InitWpf()
$this.SetupEventHandlers()
}
}
$instance = [MyClass]::new()
$instance.Run()

Get XML node children using a variable

Given the following file:
<environments>
<dev>
<property1>23</property1>
<property2>blue</property2>
<property3>apple</property3>
</dev>
<prod>
<property1>27</property1>
<property2>red</property2>
<property3>orange</property3>
</prod>
</environments>
And the following code:
$environmentsFile = [System.Xml.XmlDocument](Get-Content "environments.xml");
$envs = $environmentsFile.environments;
$envName = "prod"
How would I access the "prod" children and iterate through them? I'm already iterating through the environments using ForEach ($env in $envs.ChildNodes) {} and I know I oculd do $envs.prod
I tried this:
$test = $envs.SelectNodes("//$envName")
ForEach ($ele in $test) {
Write-Host $ele.value
}
But it didn't output anything. Inspecting reveals that it looks like $test has a single node that contains all the nodes I want, so I tried $test[0] and that didn't work either.
I had done a couple things wrong. This functions as desired:
$test = $envs.SelectNodes("//$envName")[0]
ForEach ($ele in $test.ChildNodes) {
Write-Host "$($ele.Name): $($ele.InnerText)"
}

Simple way to copy node from existing XML-File to New XML-File

I have an existing (source-)XML file which looks like this:
<?xml version="1.0" encoding="utf-8"?>
<Base>
<Meta>
<Element>Value</Element>
</Meta>
<Settings>
<Hostname>Computer01</Hostname>
</Settings>
<Data>
<Element>Value</Element>
</Data>
</Base>
I import this XML-file to the Variable "$Xml" with the following PowerShell-Code:
$Xml = New-Object -TypeName System.Xml.XmlDocument ; $Xml.Load( $PathToXml )
I want to export/save the <Settings> node (+children) from $Xml to a new XML-File, that does not exist yet.
As for this example the new destination-XML file should look somehow like this:
<?xml version="1.0" encoding="utf-8"?>
<Base>
<Settings>
<Hostname>Computer01</Hostname>
</Settings>
</Base>
My temporary solution is to copy over the whole source-xml and select and remove the unwanted nodes, but this is not a clean solution. I also tried to build up a new XML from scratch, but that used a lot of code and I am sure, there is a cleaner and simpler way to get this task done.
Thank you in advance
The following will accomplish what you need if you are okay with using XPath syntax.
$xml = [xml](Get-Content source.xml)
$Nodes = $xml.SelectNodes('Base/*[name() != "Settings"]')
foreach ($Node in $Nodes) {
[void]$xml.Base.RemoveChild($Node)
}
$xml.Save("c:\temp\destination.xml")
An alternative, relying on the where() method to find the node names to remove, is the following.
$xml = [xml](Get-Content source.xml)
$NodesToRemove = $xml.Base.ChildNodes.Name.where{$_ -ne 'Settings'}
foreach ($NodeName in $NodesToRemove) {
$node = $xml.Base.SelectSingleNode($NodeName)
[void]$xml.Base.RemoveChild($node)
}
$xml.Save("c:\temp\destination.xml")
Explanation:
$xml stores the XML contents as an XmlDocument type by use of the [xml] type accelerator. $NodesToRemove is an array that holds the names of all child nodes of Base not named Settings. Then the nodes matching the names within the array are found and subsequently removed. Save() is used to store the updated XML content.

Build Once, Deploy Everywhere with Web Deploy and .NET Web.configs

I am working on setting up a continuous build and deploy system that will manage builds and deployments of our .NET applications to multiple environments. We want to do this so that we build, deploy that build to our development environment, and at a later time have the option to deploy that same build to our test environment with different config file settings. Currently our developers are used to using web.config transforms to manage the config values for each environment and they would prefer to continue doing it that way. Finally, we want to do our deployments with MS Web Deploy 3.6 and its package deploy option.
After doing some research we have found and considered the following options:
Use the Web Deploy parameterization feature to change the config files at deploy time. This would replace the web.config transformations which we would like to avoid.
Run MSBuild once per project configuration/web.config transform to produce a package containing a transformed web.config for each environment. This has the downside of increasing build times and storage requirements for our packages.
Use both Web Deploy parameterization and web.config transformations. This allows developers to continue to use web.configs to debug other environments as they have been and avoids creating multiple packages, but requires us to maintain config settings in multiple places.
At build time, use the web.config transformations to generate multiple config files but only one package and at deploy time use a script to insert the proper config into the correct location of in the package. This seems easier said than done since it's not the way Web Deploy was designed to work and our on initial evaluation appears complicated to implement.
Are there any other options that we haven't considered? Is there a way to do this that allows us to keep using web.configs as we have been but only generate a single Web Deploy package?
In .NET 4.7.1, another option is possible: using ConfigurationBuilder.
The idea is that a custom class has the opportunity to manipulate the values contained in the web.config before they are passed to the application. This allows to plug in other configuration systems.
For example: Using a similar approach to configuration as ASP.NET Core, the NuGet packages it includes can be used independently on .NET Framework as well to load json and override json files. Then an environment variable (or any other value like IIS app pool ID, machine name etc.) can be used to determine which override json file to use.
For example: If there was an appsettings.json file like
{
"appSettings": { "Foo": "FooValue", "Bar": "BarValue" }
}
and an appsettings.Production.json file containing
{
"appSettings": { "Foo": "ProductionFooValue" }
}
One could write a config builder like
public class AppSettingsConfigurationBuilder : ConfigurationBuilder
{
public override ConfigurationSection ProcessConfigurationSection(ConfigurationSection configSection)
{
if(configSection is AppSettingsSection appSettingsSection)
{
var appSettings = appSettingsSection.Settings;
var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
var appConfig = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile($"appsettings.{environmentName}.json", optional: true)
.Build();
appSettings.Add("Foo", appConfig["appSettings:Foo"]);
appSettings.Add("Bar", appConfig["appSettings:Bar"]);
}
return configSection;
}
}
and then wire up the config builder in Web.config:
<configSections>
<section name="configBuilders" type="System.Configuration.ConfigurationBuildersSection, System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false"/>
</configSections>
<configBuilders>
<builders>
<add name="AppSettingsConfigurationBuilder" type="My.Project.AppSettingsConfigurationBuilder, My.Project"/>
</builders>
</configBuilders>
<appSettings configBuilders="AppSettingsConfigurationBuilder" />
If you then set the ASPNETCORE_ENVIRONMENT (name only chosen so it ASP.NET Core Apps on the same server would use the same be default) environment variable to Development on dev machines, ConfigurationManager.AppSettings["Foo"] would see FooValue instead of FooProductionValue.
You could also use the APP_POOL_ID to hardcode environment names or use the IIS 10 feature to set environment variables on app pools. This way you can truly build once and copy the same output to different servers or even to multiple directories on the same server and still use a different config for different server.
I don't know if it's less complicated than option 4 above, but the solution we are going with is to run a PowerShell script immediately prior to running MSBuild which parses the web.config transforms and either generates or augments the parameters.xml file. This gives us the flexibility to use parameterization and it's ability to modify config files other than the web.config while preserving 100% of the current functionality of the web.config transformations. Here is the script we are currently using for the benefit of future seekers:
function Convert-XmlElementToString
{
[CmdletBinding()]
param([Parameter(Mandatory=$true)] $xml, [String[]] $attributesToExclude)
$attributesToRemove = #()
foreach($attr in $xml.Attributes) {
if($attr.Name.Contains('xdt') -or $attr.Name.Contains('xmlns') -or $attributesToExclude -contains $attr.Name) {
$attributesToRemove += $attr
}
}
foreach($attr in $attributesToRemove) { $removedAttr = $xml.Attributes.Remove($attr) }
$sw = New-Object System.IO.StringWriter
$xmlSettings = New-Object System.Xml.XmlWriterSettings
$xmlSettings.ConformanceLevel = [System.Xml.ConformanceLevel]::Fragment
$xmlSettings.Indent = $true
$xw = [System.Xml.XmlWriter]::Create($sw, $xmlSettings)
$xml.WriteTo($xw)
$xw.Close()
return $sw.ToString()
}
function BuildParameterXml ($name, $match, $env, $value, $parameterXmlDocument)
{
$existingNode = $parameterXmlDocument.selectNodes("//parameter[#name='$name']")
$value = $value.Replace("'","&apos;") #Need to make sure any single quotes in the value don't break XPath
if($existingNode.Count -eq 0){
#no existing parameter for this transformation
$newParamter = [xml]("<parameter name=`"" + $name + "`">" +
"<parameterEntry kind=`"XmlFile`" scope=`"\\web.config$`" match=`"" + $match + "`" />" +
"<parameterValue env=`"" + $env + "`" value=`"`" />" +
"</parameter>")
$newParamter.selectNodes('//parameter/parameterValue').ItemOf(0).SetAttribute('value', $value)
$imported=$parameterXmlDocument.ImportNode($newParamter.DocumentElement, $true)
$appendedNode = $parameterXmlDocument.selectNodes('//parameters').ItemOf(0).AppendChild($imported)
} else {
#parameter exists but entry is different from an existing entry
$entryXPath = "//parameter[#name=`"$name`"]/parameterEntry[#kind=`"XmlFile`" and #scope=`"\\web.config$`" and #match=`"$match`"]"
$existingEntry = $parameterXmlDocument.selectNodes($entryXPath)
if($existingEntry.Count -eq 0) { throw "There is web.config transformation ($name) that conflicts with an existing parameters.xml entry" }
#parameter exists but environment value is different from an existing environment value
$envValueXPath = "//parameter[#name='$name']/parameterValue[#env='$env' and #value='$value']"
$existingEnvValue = $parameterXmlDocument.selectNodes($envValueXPath)
$existingEnv = $parameterXmlDocument.selectNodes("//parameter[#name=`"$name`"]/parameterValue[#env=`"$env`"]")
if($existingEnvValue.Count -eq 0 -and $existingEnv.Count -gt 0) {
throw "There is web.config transformation ($name) for this environment ($env) that conflicts with an existing parameters.xml value"
} elseif ($existingEnvValue.Count -eq 0 -and $existingEnv.Count -eq 0) {
$newParamter = [xml]("<parameterValue env=`"" + $env + "`" value=`"`" />")
$newParamter.selectNodes('//parameterValue').ItemOf(0).SetAttribute('value', $value)
$imported=$parameterXmlDocument.ImportNode($newParamter.DocumentElement, $true)
$appendedNode = $existingNode.ItemOf(0).AppendChild($imported)
}
}
}
function UpdateSetParams ($node, $originalXml, $path, $env, $parametersXml)
{
foreach ($childNode in $node.ChildNodes)
{
$xdtValue = ""
$name = ""
$match = ($path + $childNode.toString())
if($childNode.Attributes -and $childNode.Attributes.GetNamedItem('xdt:Locator').Value) {
$hasMatch = $childNode.Attributes.GetNamedItem('xdt:Locator').Value -match ".?\((.*?)\).*"
$name = $childNode.Attributes.GetNamedItem($matches[1]).Value
$match = $match + "[#" + $matches[1] + "=`'" + $name + "`']"
}
if($childNode.Attributes -and $childNode.Attributes.GetNamedItem('xdt:Transform')) {
$xdtValue = $childNode.Attributes.GetNamedItem('xdt:Transform').Value
}
if($xdtValue -eq 'Replace') {
if($childNode.Attributes.GetNamedItem('xdt:Locator').Value) {
$hasMatch = $childNode.Attributes.GetNamedItem('xdt:Locator').Value -match ".?\((.*?)\).*"
$name = $childNode.Attributes.GetNamedItem($matches[1]).Value
} else {
$name = $childNode.toString()
}
$nodeString = Convert-XmlElementToString $childNode.PsObject.Copy()
BuildParameterXml $name $match $env $nodeString $parametersXml
} elseif ($xdtValue.Contains('RemoveAttributes')) {
if($originalXml.selectNodes($match).Count -gt 0) {
$hasMatch = $xdtValue -match ".?\((.*?)\).*"
$nodeString = Convert-XmlElementToString $originalXml.selectNodes($match).ItemOf(0).PsObject.Copy() $matches[1].Split(',')
$newParamter = BuildParameterXml $childNode.toString() $match $env $nodeString $parametersXml
$newParamters += $newParamter
}
} elseif ($xdtValue.Contains('SetAttributes')) {
if($originalXml.selectNodes($match).Count -gt 0) {
$nodeCopy = $originalXml.selectNodes($match).ItemOf(0).PsObject.Copy()
$hasMatch = $xdtValue -match ".?\((.*?)\).*"
foreach($attr in $matches[1].Split(',')){
$nodeCopy.SetAttribute($attr, $childNode.Attributes.GetNamedItem($attr).Value)
}
$nodeString = Convert-XmlElementToString $nodeCopy
BuildParameterXml $childNode.toString() "($match)[1]" $env $nodeString $parametersXml
}
} elseif ($xdtValue) {
throw "Yikes! the script doesn't know how to handle this transformation!"
}
#Recurse into this node to check if it has transformations on its children
if($childNode) {
UpdateSetParams $childNode $originalXml ($match + "/") $env $parametersXml
}
}
}
function TransformConfigsIntoParamters ($webConfigPath, $webConfigTransformPath, $parametersXml)
{
#Parse out the environment names
$hasMatch = $webConfigTransformPath -match ".?web\.(.*?)\.config.*"
[xml]$transformXml = Get-Content $webConfigTransformPath
[xml]$webConfigXml = Get-Content $webConfigPath
UpdateSetParams $transformXml $webConfigXml '//' $matches[1] $parametersXml
}
$applicationRoot = $ENV:WORKSPACE
if(Test-Path ($applicationRoot + '\parameters.xml')) {
[xml]$parametersXml = Get-Content ($applicationRoot + '\parameters.xml')
$parametersNode = $parametersXml.selectNodes('//parameters').ItemOf(0)
} else {
[System.XML.XMLDocument]$parametersXml=New-Object System.XML.XMLDocument
[System.XML.XMLElement]$parametersNode=$parametersXml.CreateElement("parameters")
$appendedNode = $parametersXml.appendChild($parametersNode)
}
TransformConfigsIntoParamters ($applicationRoot + '\web.config') ($applicationRoot + '\web.Development.config') $parametersXml
TransformConfigsIntoParamters ($applicationRoot + '\web.config') ($applicationRoot + '\web.SystemTest.config') $parametersXml
TransformConfigsIntoParamters ($applicationRoot + '\web.config') ($applicationRoot + '\web.Production.config') $parametersXml
$parametersXml.Save($applicationRoot + '\parameters.xml')

How do I represent properties of PowerShell objects in an inline string without workarounds?

How do I represent properties of objects in an inline string without workarounds?
I know that I can represent complex variable names, names with dots, like so: ${My bad.variable-name} so why wouldn't it work for object properties ${Xml.Node.Value}?
[xml]$appConfig = #"
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="TerminalID" value="999999" />
<add key="ClientID" value="TESTTEST" />
</appSettings>
</configuration>
"#
$appSettings = Select-Xml $appConfig -XPath '//add'
foreach ($setting in $appSettings)
{
$setting.Node.key #works
$setting.Node.value #works
#I want to display above values as part of a string
"Setting: $setting.Node.key = Value: $setting.Node.value"
#ah, I need curly braces around the variable
${setting.Node.key} #doesn't work
${setting.Node.value} #doesn't work
"Setting: ${setting.Node.key} = Value: ${setting.Node.value}" #definately doesn't work
#I think it's because I'm referring to the property (key) of a property (Node) of a variable (settting)
$setting | gm -MemberType Property
$setting.Node | gm -MemberType Property
#I can solve with string::format
[string]::format("Setting: {0} = Value: {1}", $setting.Node.key, $setting.Node.value)
#But I like inline strings in powershell so I have to do this, how can I do it inline without reassigning to another variable?
$key = $setting.Node.key
$value = $setting.Node.value
"Setting: $key = Value: $value" #works
}
{} makes it possible to specify a "special" variable name only. To access an object's properties inside a string, you need to use a subexpression #(), like
"Setting: $($setting.Node.key) = Value: $($setting.Node.value)"
Subexpressions are evaluated before string creation, so they will be replaced with the values of the script inside $()
Have you tried $() instead of ${}?
$($setting.Node.key)
http://blogs.technet.com/b/heyscriptingguy/archive/2014/02/15/string-formatting-in-windows-powershell.aspx