Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Subscribe
Archives
November 14, 2025

Pretty Easy

In this issue:

  • Get-CimProcess
  • Type Data Consolidation
  • Property Sets
  • Extending Types XML
    • Alias Properties
    • Script Properties
    • Script Methods
    • Using Export-PSTypeExtension
  • Summary

Over the last few issues, we've been exploring how to do more with object output in PowerShell. There isn't a single best practice. Much depends on how you are constructing your object output, and how you think it might be consumed. Don't assume that the person using your code will run it the same way you do. You might be that person tomorrow that has a new use case. There is an art to designing and refining an object that comes with experience. I've been trying to demonstrate some techniques you can use to implement your vision.

Get-CimProcess

For today's work, I took the code I was using last time into a PowerShell function.

#requires -version 7.5
#requires -module Microsoft.PowerShell.ThreadJob
function Get-CimProcess {
    [CmdletBinding()]
    [OutputType('myCimProcess')]
    param(
        [Parameter(
            Position = 0,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName,
            HelpMessage = 'Specify the name of a remote computer.'
        )]
        [Alias('CN')]
        [ValidateNotNullOrEmpty()]
        [string]$ComputerName = $env:COMPUTERNAME,
        [Parameter(HelpMessage = "Specify the thread job throttle limit.")]
        [string]$ThrottleLimit = 20
    )
    begin {
        #use a timer to track how long the function takes to run
        $timer = [System.Diagnostics.Stopwatch]::new()
        $timer.Start()

        Write-Verbose "[$((Get-Date).TimeOfDay) BEGIN  ] Starting $($MyInvocation.MyCommand)"
        #define a global variable for the check date property
        $lastCheck = Get-Date
        Write-Verbose "[$((Get-Date).TimeOfDay) BEGIN  ] Using a LastCheck value of $lastCheck"

        #define the script block to be run in the thread job
        $sb = {
            param($cimProcess)
            #suppress errors when no username is available
            #write-host "[$((Get-Date).TimeOfDay)] Getting owner for process $($cimProcess.ProcessID)" -ForegroundColor cyan
            $r = Invoke-CimMethod -InputObject $cimProcess -MethodName GetOwner -ErrorAction SilentlyContinue
            @{
                PSTypeName   = 'myCimProcess'
                ProcessID    = $cimProcess.ProcessId -as [int]
                Name         = $cimProcess.Name
                CreationDate = $cimProcess.CreationDate
                Path         = $cimProcess.Path -as [string]
                CommandLine  = $cimProcess.CommandLine -as [string]
                Handles      = $cimProcess.HandleCount -as [int]
                Threads      = $cimProcess.ThreadCount -as [int]
                Username     = $r.ReturnValue -eq 0 ? ('{0}\{1}' -f $r.Domain, $r.User) : $Null
                Computername = $cimProcess.CSName
                LastCheck    = $using:lastCheck
            }
        }
    } #begin
    process {
        Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Getting Cim Process information from $($Computername.ToUpper())"
        Get-CimInstance -ClassName win32_process -Filter 'ProcessID >10' -ComputerName $ComputerName |
        ForEach-Object -Begin {
            Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Getting process information using thread jobs"
            $jobs = @()
        } -Process {
            $jobs += Start-ThreadJob -ScriptBlock $sb -ArgumentList $_ -ThrottleLimit $ThrottleLimit #-StreamingHost $PSHost
        } -End {
            Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Waiting for $($jobs.Count) thread jobs to complete"
        }
    } #process
    end {
        #wait for jobs to complete
        $jobs | Receive-Job -Wait -AutoRemoveJob | ForEach-Object {
            #create the job outside of the thread job
            New-Object -TypeName PSObject -Property $_
        }
        Write-Verbose "[$((Get-Date).TimeOfDay) END    ] All jobs completed"

        #Stop the timer
        $timer.Stop()
        Write-Verbose "[$((Get-Date).TimeOfDay) END    ] Runtime $($timer.Elapsed)"
        Write-Verbose "[$((Get-Date).TimeOfDay) END    ] Ending $($MyInvocation.MyCommand)"
    } #end
}

The code is slightly restructured from my console-based version, but performs the same way.

PS C:\> $p = Get-CimProcess -Verbose -ComputerName cadenza
VERBOSE: [14:33:34.7872447 BEGIN  ] Starting Get-CimProcess
VERBOSE: [14:33:34.7875905 BEGIN  ] Using a LastCheck value of 11/06/2025 14:33:34
VERBOSE: [14:33:34.7878543 PROCESS] Getting Cim Process information from CADENZA
VERBOSE: [14:33:34.7898053 PROCESS] Getting process information using thread jobs
VERBOSE: Perform operation 'Query CimInstances' with following parameters, ''queryExpression' = SELECT * FROM win32_process WHERE ProcessID >10,'queryDialect' = WQL,'namespaceName' = root\cimv2'.
VERBOSE: Operation 'Query CimInstances' complete.
VERBOSE: [14:33:35.3661219 PROCESS] Waiting for 281 thread jobs to complete
VERBOSE: [14:33:48.0842883 END    ] All jobs completed
VERBOSE: [14:33:48.0846193 END    ] Runtime 00:00:13.2973861
VERBOSE: [14:33:48.0848929 END    ] Ending Get-CimProcess

PS C:\> $p[240]

Computername : CADENZA
CreationDate : 11/5/2025 5:20:43 PM
Username     : Cadenza\jeff
LastCheck    : 11/6/2025 2:33:34 PM
Handles      : 159
ProcessID    : 13108
Name         : RuntimeBroker.exe
Threads      : 2
Path         : C:\Windows\System32\RuntimeBroker.exe
CommandLine  : C:\Windows\System32\RuntimeBroker.exe -Embedding

Type Data Consolidation

To get us back to where we left off, I also need to add the type extensions for the alias properties and other features. Instead of repeating the splatting code with Update-TypeData, I "exported" the extensions to a JSON file.

$list = [System.Collections.Generic.List[hashtable]]::new()
$paramHash = @{
    TypeName   = 'myCimProcess'
    MemberType = 'AliasProperty'
    MemberName = 'Started'
    Value      = 'CreationDate'
    Force      = $True
}

$list.Add($paramHash)

#this changes the stored hashtable
#$paramHash["MemberName"] = 'ID'
#$paramHash["Value"] = 'ProcessID'
$paramHash = @{
    TypeName   = 'myCimProcess'
    MemberType = 'AliasProperty'
    MemberName = 'ID'
    Value      = 'ProcessID'
    Force      = $True
}

$list.Add($paramHash)

$paramHash = @{
    TypeName   = 'myCimProcess'
    MemberType = 'ScriptProperty'
    MemberName = 'RunTime'
    Value      = { New-TimeSpan -Start $this.CreationDate -End (Get-Date) }
    Force      = $True
}
$list.Add($paramHash)

$paramHash = @{
    TypeName   = 'myCimProcess'
    MemberType = 'ScriptMethod'
    MemberName = 'Refresh'
    Value      = {
    #assumes a valid credential
    $r = Get-CimInstance -ClassName win32_process -Filter "ProcessID=$($this.ProcessID)" -ComputerName $this.Computername
    If ($r.ProcessID) {
        $this.Handles = $r.HandleCount
        $this.Threads = $r.ThreadCount
        $this.LastCheck = Get-Date
        $this
    }
    else {
        Write-Warning "Process ID $($this.ProcessID) was not found on $($this.Computername)"
    }
}
    Force      = $True
}
$list.Add($paramHash)

$list| ConvertTo-Json -Depth 1 -WarningAction SilentlyContinue | Out-File .\cimprocess-types.json -Encoding utf8

One difference from last time is that instead of redefining a hashtable key, I'm creating a new hashtable for each type extension. Otherwise, PowerShell does a weird thing where it updates the existing hashtable instead of creating a new one. Sort of a PowerShell quantum entanglement. But I only have to run this once to create the JSON file.

In my script file, I can import the JSON file and run Update-TypeData.

$json =  Join-Path -path $PSScriptRoot -ChildPath cimprocess-types.json

If (Test-Path $json) {
    Get-Content  -path $json | ConvertFrom-Json |
    ForEach-Object {
        #Create a scriptblock from the value for ScriptProperty and ScriptMethod types
        $paramHash = @{
            TypeName   = $_.TypeName
            MemberType = $_.MemberType
            MemberName = $_.MemberName
            Value      = ($_.MemberType -match "Script") ? [scriptblock]::Create($_.Value) : $_.Value
            Force      = $True
        }
        Update-TypeData @paramHash
    }
}
else {
    Write-Warning "Failed to find type extension data in $json"
}

This immediately applies the type extensions to my custom type.

PS C:\> $p[240]

Computername : CADENZA
CreationDate : 11/5/2025 5:20:43 PM
Username     : Cadenza\jeff
LastCheck    : 11/6/2025 2:33:34 PM
Handles      : 159
ProcessID    : 13108
Name         : RuntimeBroker.exe
Threads      : 2
Path         : C:\Windows\System32\RuntimeBroker.exe
CommandLine  : C:\Windows\System32\RuntimeBroker.exe -Embedding
Started      : 11/5/2025 5:20:43 PM
ID           : 13108
RunTime      : 21:21:42.8296093

I'll skip setting default property sets because eventually I will add default formatting.

Want to read the full issue?
GitHub Bluesky LinkedIn Mastodon
Powered by Buttondown, the easiest way to start and grow your newsletter.