PowerShell – Update-Object – Function to add/update object properties

It is not that no one could write this function. In fact, anyone could, but no one would! If someone did, everyone would use it – a function to add/update properties of an object.

More precisely, a function that

  • Adds new properties
  • Do appropriate checks to see property existence
  • Ignore or alert if properties are missing
  • Default $null values to given value
  • Override properties with other values

This function is also uses a function that does set operations (about which I wrote before)

https://sqljana.wordpress.com/2015/09/23/perform-set-operations-union-intersection-minus-complement-using-powershell/

What are some use-cases?

Consider this as a more powerful Add-Member but with checks to see property existence etc.

  • Add additional properties to objects before persisting (maybe to a database)
  • Override certain property values before passing objects on to do other things
  • Default $null valued properties to a sensible value

Although it is called Update-Object, it actually makes a copy of the object, updates the copy and returns that. Here it is:

<#
.SYNOPSIS
    Updates an object by setting properties. Can add new properties

.DESCRIPTION 

    Given an object and set of properties to default or override (and optionally add), does so

.INPUTS
    one or more objects with properties

.OUTPUTS
    The results of the set operation

.EXAMPLE 

    $process = (Get-Process)[0]
    $processUpdateed = Update-Object `
                            -InputObject @($process) `
                            -OverrideColumns @{'UpdateUser' = $env:USERNAME; 'UpdateDate' = Get-Date;} `
                            -OverrideColumnsAddIfMissing: $true `
                            -OverrideColumnsIgnoreIfMissing:$true `
                            -DefaultColumns @{'TestProperty'='defaulted test'} `
                            -DefaultColumnsIgnoreIfMissing:$true `
                            -DefaultColumnsAddIfMissing:$true

    $processUpdateed | Select-Object UpdateUser, UpdateDate, TestProperty

.NOTES
    Pretty self-explanatory. Depends on Get-SetOperationResult function.

Version History
    v1.0 - 20151119 - Created by Jana Sattainathan | Twitter @SQLJana | WordPress: SQLJana.WordPress.com

.LINK
#>
function Update-Object
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true,
                   Position=0)]
        [object[]]$InputObject,

        #Main options
        [Parameter(Mandatory=$false)] [HashTable]$DefaultColumns = @{},
        [Parameter(Mandatory=$false)] [Switch]$DefaultColumnsIgnoreIfMissing = $false,
        [Parameter(Mandatory=$false)] [Switch]$DefaultColumnsAddIfMissing = $false,        

        [Parameter(Mandatory=$false)] [HashTable]$OverrideColumns = @{},
        [Parameter(Mandatory=$false)] [Switch]$OverrideColumnsIgnoreIfMissing = $false,
        [Parameter(Mandatory=$false)] [Switch]$OverrideColumnsAddIfMissing = $true
    )

    BEGIN
    {
        [bool]$first = $false
    }

    PROCESS
    {
        [string] $fn = $MyInvocation.MyCommand
        [string[]] $inputColumns = @()

        #Validations
        #RCM: what if the first property is not reflective of all the properties?  Unlikely, but...
        if ($first)
        {
            $first = $false
            #Add parameter validations if there are any...

            #Make sure that the object is not $null
            if ($InputObject -eq $null)
            {
                throw [System.ArgumentException] "[$fn]: Null value specified for argument `$InputObject!"
            }

            #Make sure that the input array has atleast one element
            if ($InputObject.Count -eq 0)
            {
                throw [System.ArgumentException] "[$fn]: No elements are in array argument `$InputObject!"
            }

            #Make sure that overlapping columns are not specified for defaulting and overriding!
            $overlappingColumns = Get-SetOperationResult `
                        -Left $DefaultColumns.Keys `
                        -Right $OverrideColumns.Keys `
                        -OperationType Intersection

            if ($overlappingColumns -ne $null)
            {
                throw [System.ArgumentException] "[$fn]: Same columns specified  for both defaulting and override: [{0}]!" -f ($overlappingColumns -join ',')
            }
        }

        foreach($object in $InputObject)
        {
            [object]$result = $object | Select-object *

            #Get the list of input columns on the object
            $inputColumns = Get-Member -InputObject $result -MemberType Properties | Select-Object -Expand Name

            #----------------------------------
            #Override the value if necessary
            #----------------------------------
            foreach($overrideColumn in $OverrideColumns.Keys)
            {
                #If the column exists, override
                if ($inputColumns.Contains($overrideColumn))
                {
                    $result | %{$_.$overrideColumn = $OverrideColumns[$overrideColumn]}
                }
                else
                {
                    #Ignore missing override column if we can
                    if ($OverrideColumnsIgnoreIfMissing -eq $false)
                    {
                        #We cannot ignore that the override column is missing in the input object!
                        throw [System.ArgumentException] "[$fn]: Expected override column [$overrideColumn] in `$InputObject!"
                    }
                    else
                    {
                        #Add the override column if asked to (and it is missing in the input object)
                        if ($OverrideColumnsAddIfMissing -eq $true)
                        {
                            $result | Add-Member -MemberType NoteProperty -Name $overrideColumn -Value $OverrideColumns[$overrideColumn]
                        }
                    }
                }
            }

            #We CANNOT override and default. Override takes precedence. That is why we do not re-fetch the properties
            #$inputColumns = Get-Member -InputObject $object -MemberType Properties | Select-Object -Expand Name

            #----------------------------------
            #Default the value if necessary
            #----------------------------------
            foreach($defaultColumn in $DefaultColumns.Keys)
            {
                #If the column exists
                if ($inputColumns.Contains($defaultColumn))
                {
                    #If the value is null, default
                    #Cannot do this if the value is null. Will get error "[214,40: Select-Object] Cannot process argument because the value of argument "obj" is null. Change the value of argument "obj" to a non-null value"
                    #$value = $result | Select-Object -ExpandProperty $defaultColumn

                    $value = $result | Foreach-Object { $_.$($defaultColumn)}

                    if ($value -eq $null)
                    {
                        $result | %{$_.$defaultColumn = $DefaultColumns[$defaultColumn]}
                    }
                }
                else
                {
                    #Ignore missing override column if we can
                    if ($DefaultColumnsIgnoreIfMissing -eq $false)
                    {
                        #We cannot ignore that the default column is missing in the input object!
                        throw [System.ArgumentException] "[$fn]: Expected default column [$defaultColumn] in `$InputObject!"
                    }
                    else
                    {
                        #Add the override column if asked to (and it is missing in the input object)
                        if ($DefaultColumnsAddIfMissing -eq $true)
                        {
                            $result | Add-Member -MemberType NoteProperty -Name $defaultColumn -Value $DefaultColumns[$defaultColumn]
                        }
                    }
                }
            }
        }

        Write-Output $result
    }

    END
    {
    }
}

For example, let us manipulate the first Process object returned by the Get-Process function and add a couple of new properties to the object named UpdateUser and UpdateDate. The key thing is you could run this over and over and it would not error out and you do not have to do any additional checks:

    $process = (Get-Process)[0]
    $processUpdated = Update-Object `
                    -InputObject @($process) `
                    -OverrideColumns @{'UpdateUser' = $env:USERNAME; 'UpdateDate' = Get-Date;} `
                    -OverrideColumnsAddIfMissing: $true `
                    -OverrideColumnsIgnoreIfMissing:$true `
                    -DefaultColumns @{'TestProperty'='defaulted test'} `
                    -DefaultColumnsIgnoreIfMissing:$true `
                    -DefaultColumnsAddIfMissing:$true

    $processUpdated | Select-Object UpdateUser, UpdateDate, TestProperty | ft -auto

Returns:

UpdateUser  UpdateDate            TestProperty
----------  ----------            ------------
 11/20/2015 7:34:41 AM defaulted test

Done! Just try to think how complicated code can become if you want to add properties on the fly and you have to check before adding (not to mention overriding, after checking for existence). Here, I am just trying to add a “ConnectionString” property to an object!

$result = $object

#Check if ConnectionString property already exists (possibly due to refetching!)
$resultColumns = Get-Member -InputObject $result -MemberType Properties | Select-Object -Expand Name
if ($resultColumns.Contains('ConnectionString'))
{
    $result.ConnectionString = $connectionString
}
else
{
    $result | Add-Member -MemberType NoteProperty -Name ConnectionString -value $connectionString
}

There are better ways to do this with PowerShell v5 but above code would work in older versions too. You may use the code as you see fit. Use at your own risk! Performance has not been tested. I would not use it work with more than a handful of objects!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s