Powershell class implement get set property - powershell

How can I implement a get/set property with PowerShell class?
Please have a look on my example below:
Class TestObject
{
[DateTime]$StartTimestamp = (Get-Date)
[DateTime]$EndTimestamp = (Get-Date).AddHours(2)
[TimeSpan] $TotalDuration {
get {
return ($this.EndTimestamp - $this.StartTimestamp)
}
}
hidden [string] $_name = 'Andreas'
[string] $Name {
get {
return $this._name
}
set {
$this._name = $value
}
}
}
New-Object TestObject

You can use Add-Member ScriptProperty to achieve a kind of getter and setter:
class c {
hidden $_p = $($this | Add-Member ScriptProperty 'p' `
{
# get
"getter $($this._p)"
}`
{
# set
param ( $arg )
$this._p = "setter $arg"
}
)
}
Newing it up invokes the initializer for $_p which adds scriptproperty p:
PS C:\> $c = [c]::new()
And using property p yields the following:
PS C:\>$c.p = 'arg value'
PS C:\>$c.p
getter setter arg value
This technique has some pitfalls which are mostly related to how verbose and error-prone the Add-Member line is. To avoid those pitfalls, I implemented Accessor which you can find here.
Using Accessor instead of Add-Member does an amount of error-checking and simplifies the original class implementation to this:
class c {
hidden $_p = $(Accessor $this {
get {
"getter $($this._p)"
}
set {
param ( $arg )
$this._p = "setter $arg"
}
})
}

Here's how I went about it
[string]$BaseCodeSignUrl; # Getter defined in __class_init__. Declaration allows intellisense to pick up property
[string]$PostJobUrl; # Getter defined in __class_init__. Declaration allows intellisense to pick up property
[hashtable]$Headers; # Getter defined in __class_init__. Declaration allows intellisense to pick up property
[string]$ReqJobProgressUrl; # Getter defined in __class_init__. Declaration allows intellisense to pick up property
# Powershell lacks a way to add get/set properties. This is a workaround
hidden $__class_init__ = $(Invoke-Command -InputObject $this -NoNewScope -ScriptBlock {
$this | Add-Member -MemberType ScriptProperty -Name 'BaseCodeSignUrl' -Force -Value {
if ($this.Production) { [CodeSign]::CodeSignAPIUrl } else { [CodeSign]::CodeSignTestAPIUrl }
}
$this | Add-Member -MemberType ScriptProperty -Name 'PostJobUrl' -Force -Value {
"$($this.BaseCodeSignUrl)/Post?v=$([CodeSign]::ServiceApiVersion)"
}
$this | Add-Member -MemberType ScriptProperty -Name 'Headers' -Force -Value {
#{
_ExpireInMinutes=[CodeSign]::Timeout.Minutes;
_CodeSigningKey=$this.Key;
_JobId=$this.JobId;
_Debug=$this.Dbg;
_Token=$this.Token;
}
}
$this | Add-Member -MemberType ScriptProperty -Name 'ReqJobProgressUrl' -Force -Value {
"$($this.BaseCodeSignUrl)Get?jobId=$($this.JobId)"
}
});

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$

$this object reference for nested custom objects in 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

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)

Powershell Data.Table

Can anyone give some help with powershell tables?
The working part of the script
Function CheckWMI {
Param (
[Parameter(Mandatory=$True)]
$Computers
)
$CheckWMIResults = New-Object system.Data.DataTable
$Function = $CheckWMIResults.columns.add("ComputerName", [System.Type]::GetType("System.String") )
$Function = $CheckWMIResults.columns.add("Attempts", [System.Type]::GetType("System.Int32") )
$Function = $CheckWMIResults.columns.add("Result", [System.Type]::GetType("System.String") )
ForEach ($Computer in $Computers) {
$CheckWMIResults.Rows.Add($Computer,"0","Incomplete")
}
}
CheckWMI "192.168.1.8","192.168.1.7","192.168.1.6"
As you can see it takes each of the ip addresses and create a separate row for them.
Now how can I select one of those rows and update it, such as the count column of the second row?
There is no need to use a data structure so heavy as a DataTable for this. All you need is a simple collection like an array and the generic PSObject. The following rewrites your script above, then sets the Result of the first computer to Complete:
Function CheckWMI {
Param (
[Parameter(Mandatory=$True)]
[string[]]$Computers
)
$CheckWMIResults = #();
ForEach ($Computer in $Computers) {
$TempResults = New-Object PSObject;
$TempResults | Add-Member -MemberType NoteProperty -Name "ComputerName" -Value $Computer;
$TempResults | Add-Member -MemberType NoteProperty -Name "Attempts" -Value 0;
$TempResults | Add-Member -MemberType NoteProperty -Name "Result" -Value "Incomplete";
$CheckWMIResults += $TempResults;
}
$CheckWMIResults;
}
$Results = CheckWMI -Computers "192.168.1.8","192.168.1.7","192.168.1.6"
$Results[0].Result = "Complete";
$Results;
If you do need type checking (which the DataTable gives you), define your own type.
add-type #"
public class WMIResults {
public string ComputerName;
public int Attempts;
public string Result;
}
"#
Function CheckWMI {
Param (
[Parameter(Mandatory=$True)]
[string[]]$Computers
)
$CheckWMIResults = #();
ForEach ($Computer in $Computers) {
$TempResults = New-Object WMIResults;
$TempResults.ComputerName = $Computer
$TempResults.Attempts = 0;
$TempResults.Result = "Incomplete";
$CheckWMIResults += $TempResults;
}
$CheckWMIResults;
}
$Results = CheckWMI -Computers "192.168.1.8","192.168.1.7","192.168.1.6"
$Results[0].Result = "Complete";
$Results;
See http://blogs.msdn.com/b/powershell/archive/2009/03/11/how-to-create-an-object-in-powershell.aspx and Get-Help Add-Type for more details on this second method ( you could use a struct instead of a class for trivial cases, but classes are generally a better idea).