How to create a copy of a PS object structure but not its data

This topic contains 7 replies, has 5 voices, and was last updated by  Sam Boutros 1 month ago.

  • Author
    Posts
  • #82112

    Sam Boutros
    Participant

    Here's an example of a PS object that has 3 properties, one is a 'string', one is an integer 'unit32', and one is a 'double':

    $SampleObject = [PSCustomObject][Ordered]@{
        ComputerName = $env:COMPUTERNAME
        MemoryGB     = [Math]::Round((Get-WmiObject Win32_ComputerSystem).TotalPhysicalMemory/1GB,1)
        LogicalCores = (Get-WmiObject Win32_ComputerSystem -Property NumberofLogicalProcessors).NumberofLogicalProcessors
    }
    $SampleObject | FT -a 
    $SampleObject | Get-Member
    
    ComputerName MemoryGB LogicalCores
    ------------ -------- ------------
    MGMT-066VDI      63.9           48
    
       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()              
    ComputerName NoteProperty string ComputerName=MGMT-066VDI
    LogicalCores NoteProperty uint32 LogicalCores=48         
    MemoryGB     NoteProperty double MemoryGB=63.9  
    

    I'd like to create a copy of this $Sample object that has the same members with same property types but not the data (blanks/zeros)

    I can list the object properties and identify their types as in:

    $Properties = $SampleObject | Get-Member -MemberType NoteProperty | % { 
        [PSCustomObject][Ordered]@{
            PropertyName = $_.Name
            DataType     = $_.Definition.Split(' ')[0]
        }
    }
    $Properties | FT -a 
    
    PropertyName DataType
    ------------ --------
    ComputerName string  
    LogicalCores uint32  
    MemoryGB     double  
    

    Using Add-Member and trying to type-cast properties of a new object does not work:

    $CopiedObject = New-Object -TypeName PSCustomObject
    $Properties | % { $CopiedObject | Add-Member -MemberType NoteProperty -Name $_.Name -Value [$_.DataType]0 }
    $CopiedObject | FT -a 
    

    This seems to fail because type-casting seems not to accept variables. The following attempts fail:

    $DataType = 'String'
    [$DataType]0
    [($DataType)]'0'
    [($DataType)]0
    [($DataType)]'0'
    [$($DataType)]0
    [$($DataType)]'0' 
    

    Also tried this but it copies both structure and data

    $CopiedObject = $SampleObject.psobject.Copy() 
    $CopiedObject | FT -a 
    
    ComputerName MemoryGB LogicalCores
    ------------ -------- ------------
    MGMT-066VDI      63.9           48
    

    Copying the object and changing the property values to 0 does not work because it changes property types to int:

    $CopiedObject = $SampleObject.psobject.Copy() 
    $CopiedObject | Get-Member -MemberType NoteProperty | % { $PropertyName = $_.Name; $CopiedObject.$PropertyName = 0 }
    $CopiedObject | FT -a 
    $CopiedObject | Get-Member
    
    ComputerName MemoryGB LogicalCores
    ------------ -------- ------------
               0        0            0
    
       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()             
    ComputerName NoteProperty int ComputerName=0            
    LogicalCores NoteProperty int LogicalCores=0            
    MemoryGB     NoteProperty int MemoryGB=0  
    

    I can do something like this:

    $CopiedObject = New-Object -TypeName PSCustomObject
    foreach ($Property in $Properties) { 
        switch ($Property.DataType) {
            'String' { $CopiedObject | Add-Member -MemberType NoteProperty -Name $Property.PropertyName -Value ([String]0) }
            'Uint32' { $CopiedObject | Add-Member -MemberType NoteProperty -Name $Property.PropertyName -Value ([Uint32]0) }
            'Double' { $CopiedObject | Add-Member -MemberType NoteProperty -Name $Property.PropertyName -Value ([Double]0) }
        }    
    }
    $CopiedObject | FT -a 
    $CopiedObject | Get-Member
    
    ComputerName LogicalCores MemoryGB
    ------------ ------------ --------
    0                       0        0
    
       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()             
    ComputerName NoteProperty string ComputerName=0         
    LogicalCores NoteProperty uint32 LogicalCores=0         
    MemoryGB     NoteProperty double MemoryGB=0  
    

    but then I have to anticipate and list every possible datatype. There must be a better way..

  • #82115

    Don Jones
    Keymaster

    There's not a native way to do that, really. That's because objects in .NET aren't just data structures; they're software. They can contain functional code, and the object is pointless without that. They represent an API, not a storage mechanism per se.

    You could use Get-Member to enumerate an object's properties and create a new PSObject having the same properties, as you're doing, but that's about it.

    Some types will offer a constructor to create a new instance of the object, but it won't usually be “blank.”

  • #82135

    Curtis Smith
    Participant

    You could do something like this

    $SampleObject = [PSCustomObject][Ordered]@{
        ComputerName = $env:COMPUTERNAME
        MemoryGB     = [Math]::Round((Get-WmiObject Win32_ComputerSystem).TotalPhysicalMemory/1GB,1)
        LogicalCores = (Get-WmiObject Win32_ComputerSystem -Property NumberofLogicalProcessors).NumberofLogicalProcessors
    }
    $SampleObject | FT -a | Out-Host
    $SampleObject | Get-Member | Out-Host
    
    function Copy-CleanObject {
        Param (
            [parameter(ValueFromPipeline)]
            [object]$InputObject
        )
        $objCopy = $InputObject.psobject.copy()
        Get-Member -InputObject $objCopy -MemberType NoteProperty |
        ForEach-Object {
            $datatype = $objCopy.($_.Name).GetType()
            $objCopy.($_.Name) = $null -as $datatype
        }
        $objCopy
    }
    
    $SampleObjectCopy = $SampleObject | Copy-CleanObject
    $SampleObjectCopy | FT -a | Out-Host
    $SampleObjectCopy | Get-Member | Out-Host

    Results:

    ComputerName MemoryGB LogicalCores
    ------------ -------- ------------
    CA-LAPTOP373      7.9            4
    
    
    
    
       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()               
    ComputerName NoteProperty string ComputerName=CA-LAPTOP373
    LogicalCores NoteProperty uint32 LogicalCores=4           
    MemoryGB     NoteProperty double MemoryGB=7.9             
    
    
    
    ComputerName MemoryGB LogicalCores
    ------------ -------- ------------
                        0            0
    
    
    
    
       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()             
    ComputerName NoteProperty string ComputerName=          
    LogicalCores NoteProperty uint32 LogicalCores=0         
    MemoryGB     NoteProperty double MemoryGB=0             
    
  • #82142

    Naw Awn
    Participant

    It should be simple if you just define a class? (must use PowerShell version 5)

    Class SampleObject
    {
        [String]$Computer
        [uint32]$Memory
        [double]$LogicalCores
    
        SampleObject(){}
    }
    
    $Properties = New-Object SampleObject
    
  • #82145

    Naw Awn
    Participant

    PS C:\WINDOWS\system32> $Properties | gm

    TypeName: SampleObject

    Name MemberType Definition
    —- ———- ———-
    Equals Method bool Equals(System.Object obj)
    GetHashCode Method int GetHashCode()
    GetType Method type GetType()
    ToString Method string ToString()
    Computer Property string Computer {get;set;}
    LogicalCores Property double LogicalCores {get;set;}
    Memory Property uint32 Memory {get;set;}

  • #82148

    Naw Awn
    Participant

    If you know you are getting wmi object for LogicalCore, you can use this instead

    [wmi]$LogicalCores

  • #82151

    Patrick Meinecke
    Participant

    Another option for older versions is to define a dynamic assembly:

    function New-StructureProxy {
        [OutputType([type])]
        [CmdletBinding()]
        param(
            [ValidateNotNull()]
            [psobject] $Target
        )
        end {
            $guid = [guid]::NewGuid().ToString('n')
            $assemblyName = ('DynamicAssemblyForProxy' + $guid) -as [System.Reflection.AssemblyName]
            $assembly = [System.AppDomain]::CurrentDomain.DefineDynamicAssembly(
                $assemblyName,
                [System.Reflection.Emit.AssemblyBuilderAccess]::Run)
    
            $module = $assembly.DefineDynamicModule('DynamicModuleForProxy' + $guid)
            $typeAttributes = [System.Reflection.TypeAttributes]'AutoLayout, AnsiClass, Class, Public, SequentialLayout, Sealed, BeforeFieldInit'
            $typeBuilder = $module.DefineType($guid, $typeAttributes)
    
            foreach($property in $Target.psobject.Properties) {
                $null = $typeBuilder.DefineField(
                    $property.Name,
                    $property.TypeNameOfValue -as [type],
                    [System.Reflection.FieldAttributes]::Public)
            }
    
            $typeBuilder.CreateType()
        }
    }
    

    Usage:

    $targetObject = Get-Item .
    $type = New-StructureProxy -Target $targetObject
    $instance = [Activator]::CreateInstance($type)
    
  • #82162

    Sam Boutros
    Participant

    Thank you all for the insightful replies 🙂

You must be logged in to reply to this topic.