$this object reference for nested custom objects in Powershell - powershell

There's something that I can't quite seem to wrap my head around when trying to do object references in Powershell. Not sure if there's something that I am missing out on.
A sample code illustrating this problem is as follows:
function Create-Custom-Object {
$oResult = New-Object -TypeName PSObject -Property (#{
"Test" = $(Get-Date);
})
Add-Member -memberType ScriptMethod -InputObject $oResult -Name "GetTest" -Value {
return $this.Test;
}
return $oResult
}
function Create-Wrapper-Object {
$oObject = $(Create-Custom-Object)
$oResult = New-Object -TypeName PSObject -Property (#{
"Object" = $oObject;
"Test" = $(Get-Date);
})
Add-Member -MemberType ScriptMethod -InputObject $oResult -Name "WrapTest" -Value {
return $this.Object.GetTest()
}
return $oResult
}
$oCustom = Create-Custom-Object
sleep 5
$oWrapper = Create-Wrapper-Object
echo "Custom-Test: $($oCustom.Test)"
echo "Wrapper-Test: $($oWrapper.Test)"
echo "GetTest: $($oCustom.GetTest())"
echo "WrapTest: $($oWrapper.WrapTest())"
When run, the output is as per below:
>powershell -file test.ps1
Custom-Test: 11/20/2017 16:10:19
Wrapper-Test: 11/20/2017 16:10:24
GetTest: 11/20/2017 16:10:19
WrapTest: 11/20/2017 16:10:24
What puzzled me is that the call to WrapTest() on the wrapper object returns the "Test" attribute value from the wrapper object instead of the embedded custom object. Why is Powershell behaving like this?

I suspect that the problem here (based on the assumed intent of the sleep 5) is that $oCustom is assigned a Custom-Object and then 5 seconds later $oWrapper is assigned a Wrapper-Object which contains a new Custom-Object with essentially the same [DateTime] value (to the nearest second), not the (intended?) previously created $oCustom. WrapTest() is not returning the Test member of $oWrapper but the indistinguishable Test member of its own Custom-Object in $oWrapper.Object. In order to create a (generic) wrapper object, you need something to wrap, otherwise it's really just a (specific) nested object. Something like this:
function Create-Wrapper-Object {
param ($ObjectToWrap)
$oResult = New-Object -TypeName PSObject -Property (#{
"Object" = $ObjectToWrap; # presumably with a GetTest() method
"Test" = $(Get-Date); # remember the time of wrapping
})
Add-Member -MemberType ScriptMethod -InputObject $oResult -Name "WrapTest" -Value {
return $this.Object.GetTest()
}
return $oResult
}
With the (assumed to be) desired result:
$oCustom = Create-Custom-Object
sleep 5
$oWrapper = Create-Wrapper-Object $oCustom
echo "Custom-Test: $($oCustom.Test)"
Custom-Test: 05/31/2021 08:52:30
echo "Wrapper-Test: $($oWrapper.Test)"
Wrapper-Test: 05/31/2021 08:52:35
echo "GetTest: $($oCustom.GetTest())"
GetTest: 05/31/2021 08:52:30
echo "WrapTest: $($oWrapper.WrapTest())"
WrapTest: 05/31/2021 08:52:30

Related

Add-Member to add a custom method to a PowerShell object

I want to add a custom method to an existing object. My problem is I may not find out how to make it accept parameters.
In this greatly simplified example I want to add a script block to a System.IO.FileInfo-Object to output a specific parameter to the screen:
$NewMethodScript = {
param(
[String] $Param1
)
write-host $this.$Param1
#Do lots of more stuff, call functions, etc...
}
$FInfo = [System.IO.FileInfo]::new("C:\File.txt")
$FInfo | Add-Member -MemberType ScriptMethod -Name NewMethod -Value $NewMethodScript
$FInfo.NewMethod "DirectoryName"

How do you use the default values on a PSCustomObject's ScriptMethod

I am trying to specify the value of the third parameter of the method, while still letting the second parameter in the method default.
I was able to piece this together to get it working, but I was hoping someone else had a better solution
$o=[PSCustomObject]#{};
Add-Member -MemberType ScriptMethod -InputObject $o -Name 'WrapText' -Value {
param($S,$Open='"',$Close)
if($Close){
"$Open$S$Close"
}else{
"$Open$S$Open"
}
}
$DefaultValues = #{};
$o.WrapText.Script.Ast.ParamBlock.Parameters | %{
$DefaultValues.($_.Name.ToString()) = $_.DefaultValue.Value
}
$o.WrapText('Some Text',$DefaultValues.'$Open','|')
In order to check whether an argument was bound to a parameter, you'll want to use $PSBoundParameters:
Add-Member -MemberType ScriptMethod -InputObject $o -Name 'WrapText' -Value {
param($S,$Open='"',$Close='"')
if($PSBoundParameters.ContainsKey('Close')){
"$Open$S$Close"
}else{
"$Open$S$Open"
}
}
Now the if condition is only $true if a third argument is supplied:
PS ~> $o.WrapText('abc')
"abc"
PS ~> $o.WrapText('abc',"'")
'abc'
PS ~> $o.WrapText('abc',"'",'$')
'abc$

How do I add a System.Collections.ArrayList to a PowerShell custom object?

My goal is to create a custom data object that has two discrete variables (fooName and fooUrl) and a list of fooChildren, each list item having two discrete variables variables childAge and childName.
Currently, I have this:
$fooCollection = [PSCustomObject] #{fooName=""; fooUrl=""; fooChildrenList=#()}
$fooCollection.fooName = "foo-a-rama"
$fooCollection.fooUrl = "https://1.2.3.4"
$fooChild = New-Object -TypeName PSobject
$fooChild | Add-Member -Name childAge -MemberType NoteProperty -Value 6
$fooChild | Add-Member -Name childName -MemberType NoteProperty -Value "Betsy"
$fooCollection.fooChildrenList += $fooChild
$fooChild = New-Object -TypeName PSobject
$fooChild | Add-Member -Name childAge -MemberType NoteProperty -Value 10
$fooChild | Add-Member -Name childName -MemberType NoteProperty -Value "Rolf"
$fooCollection.fooChildrenList += $fooChild
cls
$fooCollection.fooName
$fooCollection.fooUrl
foreach ($fooChild in $fooCollection.fooChildrenList)
{
(" " + $fooChild.childName + " " + $fooChild.childAge)
}
Which produces the following. So far so good
foo-a-rama
https://1.2.3.4
Betsy 6
Rolf 10
Problem: I don't like using += because as I understand it, using += results in a copy of $fooCollection.fooChildrenList being created (in whatever state it's in) each time += is executed.
So, instead of implementing fooChildrenList as #(), I want to implement fooChildrenList as New-Object System.Collections.ArrayList so I can add each row as needed. I've tried various ways of doing this in code but fooChildrenList winds up being unpopulated. For example:
$fooCollection = [PSCustomObject] #{fooName=""; fooUrl=""; fooChildrenList = New-Object System.Collections.ArrayList}
$fooCollection.fooName = "foo-a-rama"
$fooCollection.fooUrl = "https://1.2.3.4"
$fooChild.childName = "Betsy"
$fooChild.childAge = 6
$fooCollection.fooChildrenList.Add((New-Object PSObject -Property $fooChild))
$fooChild.childName = "Rolf"
$fooChild.childAge = 10
$fooCollection.fooChildrenList.Add((New-Object PSObject -Property $fooChild))
$fooCollection | get-member shows
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
fooChildrenList NoteProperty System.Collections.ArrayList fooChildrenList=
fooName NoteProperty string fooName=foo-a-rama
fooUrl NoteProperty string fooUrl=https://1.2.3.4
$fooCollection shows
fooName : foo-a-rama
fooUrl : https://1.2.3.4
fooChildrenList : {}
How do I add a System.Collections.ArrayList to a PowerShell custom object?
Well im not sure what issue you are getting it works fine for me
function New-Child([string]$Name, [int]$Age){
$Child = New-Object -TypeName PSobject
$Child | Add-Member -Name childAge -MemberType NoteProperty -Value $age -PassThru |
Add-Member -Name childName -MemberType NoteProperty -Value $name
return $child
}
$fooCollection = [PSCustomObject] #{fooName=""; fooUrl=""; fooChildrenList = New-Object System.Collections.ArrayList}
$fooCollection.fooName = "foo-a-rama"
$fooCollection.fooUrl = "https://1.2.3.4"
$fooCollection.fooChildrenList.Add((New-Child -Name "Betty" -Age 9)) | Out-Null
$fooCollection.fooChildrenList.Add((New-Child -Name "Ralf" -Age 15)) | Out-Null
$fooCollection.fooName
$fooCollection.fooUrl
foreach ($fooChild in $fooCollection.fooChildrenList)
{
" " + $fooChild.childName + " " + $fooChild.childAge
}
output
foo-a-rama
https://1.2.3.4
Betty 9
Ralf 15
The challenge is to add a copy of the $fooChild [pscustomobject] instance you're re-using every time you add to the list with .Add() (if you don't use a copy, you'll end up with all list elements pointing to the same object).
However, you cannot clone an existing [pscustomobject] (a.k.a [psobject]) instance with New-Object PSObject -Property.
One option (PSv3+) is to define the reusable $fooChild as an ordered hashtable instead, and then use a [pscustomobject] cast, which implicitly creates a new object every time:
$fooCollection = [PSCustomObject] #{ fooChildrenList = New-Object Collections.ArrayList }
# Create the reusable $fooChild as an *ordered hashtable* (PSv3+)
$fooChild = [ordered] #{ childName = ''; childAge = -1 }
# Create 1st child and add to list with [pscustomobject] cast
$fooChild.childName = 'Betsy'; $fooChild.childAge = 6
$null = $fooCollection.fooChildrenList.Add([pscustomobject] $fooChild)
# Create and add another child.
$fooChild.childName = 'Rolf'; $fooChild.childAge = 10
$null = $fooCollection.fooChildrenList.Add([pscustomobject] $fooChild)
# Output the children
$fooCollection.fooChildrenList
Note the $null = ..., which suppresses the typically unwanted output from the .Add() method call.
The above yields:
childName childAge
--------- --------
Betsy 6
Rolf 10
A slightly more obscure alternative is to stick with $fooChild as a [pscustomobject] instance and call .psobject.Copy() on it to create a clone.
ArcSet's helpful answer provides a more modular solution that creates new custom-object instances on demand via a helper function.
Finally, in PSv5+ you could define a helper class:
$fooCollection = [PSCustomObject] #{ fooChildrenList = New-Object Collections.ArrayList }
# Define helper class
class FooChild {
[string] $childName
[int] $childAge
}
# Create 1st child and add to list with [pscustomobject] cast
$null = $fooCollection.fooChildrenList.Add([FooChild] #{ childName = 'Betsy'; childAge = 6 })
# Create and add another child.
$null = $fooCollection.fooChildrenList.Add([FooChild] #{ childName = 'Rolf'; childAge = 10 })
# Output the children
$fooCollection.fooChildrenList
Note how instances of [FooChild] can be created by simply casting a hashtable that has entries matching the class property names.
Quick copy paste from something I have that I use to make some of my Arrays. I have to create the custom objects and then add them to the Array. It will need modified for your scenario but I think it will get you what you need.
[System.Collections.ArrayList]$SQL_Query_Results = #()
ForEach ($SQL_Index in $SQL_Table) {
$SQL_Array_Object = [PSCustomObject]#{
'Computer_Name' = $SQL_Table[$SQL_Index_Counter].ComputerID -replace ",", ""
'Project' = $SQL_Table[$SQL_Index_Counter].Project
'Site' = $SQL_Table[$SQL_Index_Counter].Site
'Description' = $SQL_Table[$SQL_Index_Counter].Description -replace ",", ""
'Physical_Machine' = $SQL_Table[$SQL_Index_Counter].IsPhysicalMachine
'Active' = $SQL_Table[$SQL_Index_Counter].IsActive
}
$SQL_Query_Results.Add($SQL_Array_Object) | Out-Null
}
Edited to show how Array was initially created.

Dynamically retrieve the Name of a ScriptProperty in the Get and Set block

Goal
I'm looking to utilize the new Class feature of PowerShell 5 to create a class that i can use within my project to view records of a database and create methods to link and unlink foreign keys together.
The below class is working with 2 properties but i need to add more and don't want to copy/paste, this is how it's currently working:
# Create a new instance of my Computer class
$computer = [Computer]::new([int]71)
# Change the status of the Server in the DataRow
$computer.Status = "Active"
# Update the Row in the Database
$computer.SaveChanges()
# Link this Server in the Database to a record in the Locations table
$computer.LinkLocation([int]16)
I've got the Class part and even a few Methods implemented but i'm wondering if i can reduce the amount of code needed in the class by somehow referencing the Name property of a ScriptProperty.
Class
class Computer
{
hidden [System.Data.DataRow]$_dataRow
hidden [System.Data.SqlClient.SqlDataAdapter]$_dataAdapter
hidden GetComputerDetails([int]$serverId)
{
$connectionString = "Server=HomeTestServer\DB01;Database=TestDB;Integrated Security=SSPI;"
$sqlQuery = "SELECT ComputerName, Status FROM tbComputers WHERE Id = $serverId"
$sqlConnection = New-Object -TypeName System.Data.SqlClient.SqlConnection -ArgumentList $connectionString
$sqlCommand = New-Object -TypeName System.Data.SqlClient.SqlCommand -ArgumentList $SqlQuery, $sqlConnection
$sqlConnection.Open()
$sqlAdapter = New-Object -TypeName System.Data.SqlClient.SqlDataAdapter -ArgumentList $sqlCommand
$sqlData = New-Object -TypeName System.Data.DataSet
$sqlBuilder = New-Object -TypeName System.Data.SqlClient.SqlCommandBuilder -ArgumentList $sqlAdapter
$sqlAdapter.Fill($sqlData, "Computer")
$this._dataRow = $sqlData.Tables[0].Rows[0]
$this._dataAdapter = $sqlAdapter
}
hidden [object] GetValue([string]$propertyName)
{
return $this._dataRow.$propertyName
}
hidden SetValue([string]$propertyName, $value)
{
$this._dataRow.$propertyName = $value
}
SaveChanges()
{
$this._dataAdapter.Update($this._dataRow)
}
LinkLocation([int]$locationId)
{
$serverId = $this._dataRow.Id
[void](Invoke-Sqlcmd -ServerInstance "HomeTestServer\DB01" -Database TestDB -Query "UPDATE tbComputers SET LocationId = $locationId WHERE Id = $serverId" )
}
Computer([int]$serverId)
{
$this.GetComputerDetails([int]$serverId)
$this | Add-Member -MemberType ScriptProperty -Name ComputerName -Force -Value `
{
# Is there a way to get the ScriptProperty's Name?
$this.GetValue("ComputerName")
} `
{
param
(
$value
)
# Is there a way to get the ScriptProperty's Name?
$this.SetValue("ComputerName", $value)
}
$this | Add-Member -MemberType ScriptProperty -Name Status -Force -Value `
{
# Is there a way to get the ScriptProperty's Name?
$this.GetValue("Status")
} `
{
param
(
$value
)
# Is there a way to get the ScriptProperty's Name?
$this.SetValue("Status", $value)
}
}
}
Explanation
This class essentially wraps the DataRow and DataAdapter for easy record viewing and updating as the ScriptProperty's modify the values directly on the DataRow and the Savechanges method updates the DataRow using the DataAdapter
Problem
For each property that i want to wrap in my Class, i need to make a ScriptProperty with a Getter and a Setter, the above Class has 2 properties and doesn't look to bad but what if my SQL Table had 40 properties? I really do not want to copy and paste the $this | Add-Member... line 40 times.
Ideally i'm looking for a way to loop through and create each ScriptProperty dynamically
Attempts
foreach($propertyName in $this._dataRow.Table.Columns)
{
# Looping through doesn't work, my working theory is the Get/Set block don't expand the variable when the Member is being added, only when it's being called
$this | Add-Member -MemberType ScriptProperty -Name $propertyName -Force -Value `
{
# This is the Get block
$this.GetValue($propertyName)
} `
{
param
(
$value
)
# This is the Set block
$this.SetValue($propertyName, $value)
}
}
Edit
Highly simplified example:
class Computer
{
hidden [int]$number3 = 4
hidden [int]$number2 = 13
hidden ExtractFields([string]$propertyName)
{
$this | Add-Member -MemberType ScriptProperty -Name $propertyName -Force -Value `
{
$this.GetValue($propertyName)
} `
{
param
(
$value
)
$this.SetValue($propertyName,$value)
}
}
hidden [object] GetValue($propertyName)
{
return $this.$propertyName
}
hidden SetValue($propertyName, $value)
{
$this.$propertyName = $value
}
Computer()
{
foreach($property in #('number2', 'number3'))
{
$this.ExtractFields($property)
}
}
}
Error recieved when trying to set a dynamic property (e.g `$computer.number2 = 17)
Exception setting "number2": "The property '' cannot be found on this object. Verify that the property exists and can be set."
At line:1 char:1
+ $computer.number2 = 17
+ ~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], SetValueInvocationException
+ FullyQualifiedErrorId : ScriptSetValueRuntimeException
Question
Is there a way i can dynamically create ScriptProperty's based on specified property name?
This can be done with dynamic generation of scriptblocks for the setters and getters. Suppose class Bar is defined with two methods that implement a general way to access your fields:
class Bar
{
[object] GetFieldValue ( [string] $FieldName )
{
# replace this with code that looks up your value by field name
Write-Host "Invoked GetFieldValue($FieldName)"
return "value of $FieldName"
}
SetFieldValue ( [string] $FieldName, [object] $Value )
{
# replace this with code that sets your field value
Write-Host "Invoked SetFieldValue($FieldName,$Value)"
}
}
A function can then be implemented that decorates a Bar object with dynamically-generated properties that have getters and setters that invoke those methods:
function New-Bar
{
param( [string[]]$FieldNames )
# create the new object
$outputObject = [Bar]::new()
# add a property for each field
foreach ( $fieldName in $FieldNames )
{
$getter = [scriptblock]::Create(
"`$this.GetFieldValue('$fieldName')"
)
$setter = [scriptblock]::Create(
"`param(`$p) `$this.SetFieldValue('$fieldName',`$p)"
)
$outputObject |
Add-Member ScriptProperty $fieldName $getter $setter -Force
}
# return the object
$outputObject
}
Fields can then be accessed like this:
$b = New-Bar 'field1','field2'
$b.field1 = 'my value for field 1'
$b.field1
Which outputs:
Invoked GetFieldValue(field1)
value of field1
Invoked SetFieldValue(field1,my value for field 1)

How do I create an anonymous object in PowerShell?

I want to create an object of arbitrary values, sort of like how I can do this in C#
var anon = new { Name = "Ted", Age = 10 };
You can do any of the following, in order of easiest usage:
Use Vanilla Hashtable with PowerShell 5+
In PS5, a vanilla hash table will work for most use cases
$o = #{ Name = "Ted"; Age = 10 }
Convert Hashtable to PSCustomObject
If you don't have a strong preference, just use this where vanilla hash tables won't work:
$o = [pscustomobject]#{
Name = "Ted";
Age = 10
}
Using Select-Object cmdlet
$o = Select-Object #{n='Name';e={'Ted'}},
#{n='Age';e={10}} `
-InputObject ''
Using New-Object and Add-Member
$o = New-Object -TypeName psobject
$o | Add-Member -MemberType NoteProperty -Name Name -Value 'Ted'
$o | Add-Member -MemberType NoteProperty -Name Age -Value 10
Using New-Object and hashtables
$properties = #{
Name = "Ted";
Age = 10
}
$o = New-Object psobject -Property $properties;
Note: Objects vs. HashTables
Hashtables are just dictionaries containing keys and values, meaning you might not get the expected results from other PS functions that look for objects and properties:
$o = #{ Name="Ted"; Age= 10; }
$o | Select -Property *
Further Reading
4 Ways to Create PowerShell Objects
Everything you wanted to know about hashtables
Everything you wanted to know about PSCustomObject
Try this:
PS Z:\> $o = #{}
PS Z:\> $o.Name = "Ted"
PS Z:\> $o.Age = 10
Note: You can also include this object as the -Body of an Invoke-RestMethod and it'll serialize it with no extra work.
Update
Note the comments below. This creates a hashtable.
With PowerShell 5+
Just declare as:
$anon = #{ Name="Ted"; Age= 10; }