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

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…

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.”

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             

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

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;}

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

[wmi]$LogicalCores

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)

Thank you all for the insightful replies :slight_smile: