Today let's look at how I approached the first Iron Scripter PowerShell challenge of the year. The goal of the challenge was to convert or translate an object into a PowerShell class definition. If you are new to these challenges, the journey to how you achieve the goal is more valuable than the end result. The challenges are designed to be learning exercises.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Start with a Single Object
To define a new class of object, I need a single object to serve as my template.
$obj = Get-CimInstance win32_operatingsystem -ComputerName localhost
In this particular example, I could also use Get-CimClass to list property names. But since I want to be able to define a class from any type of object, I'll ignore this alternative.
Instead I'll rely on a hidden PowerShell feature. One of the reasons PowerShell is easy to use is that it abstracts or hides a lot of the .NET developer stuff. This is great because it means you don't have to be a developer to use PowerShell. However, as you gain experience, you can peel back some of these abstractions. That's what I am going to do. I'm going to get a generic psobject property.
This is a more generic representation of the object. Let's dive deeper.
I can see the property name and its type which will be helpful in defining my class. The PowerShell class definition will look like this:
class myClass {
[typename]$PropertyName
...
}
I can define the property line using the -f operator.
$prop = $obj.PSobject.properties | Select-Object -First 1
"[{0}]`${1}" -f $prop.TypeNameOfValue,$prop.name
The {0} and {1} entries are placeholders and they are filled in using the values on the right side of the -f operator. I'm also escaping the $ so it is treated literally. The created string is [System.Boolean]$PSShowComputerName
With this in mind, I can generate a complete class version of the object.
$obj.psobject.properties | ForEach-Object -Begin {
$class = @"
class myOS {
#properties
"@
} -Process { $class += "[{0}]`${1}`n" -f $_.TypeNameOfValue, $_.name } -End { $class += "}" }
This is what I end up with.
class myOS {
properties
[System.Boolean]$PSShowComputerName
[string]$Caption
[string]$Description
[CimInstance#DateTime]$InstallDate
[string]$Name
[string]$Status
[string]$CreationClassName
[string]$CSCreationClassName
[string]$CSName
[int16]$CurrentTimeZone
[bool]$Distributed
[uint64]$FreePhysicalMemory
[uint64]$FreeSpaceInPagingFiles
[uint64]$FreeVirtualMemory
[CimInstance#DateTime]$LastBootUpTime
[CimInstance#DateTime]$LocalDateTime
[uint32]$MaxNumberOfProcesses
[uint64]$MaxProcessMemorySize
[uint32]$NumberOfLicensedUsers
[uint32]$NumberOfProcesses
[uint32]$NumberOfUsers
[uint16]$OSType
[string]$OtherTypeDescription
[uint64]$SizeStoredInPagingFiles
[uint64]$TotalSwapSpaceSize
[uint64]$TotalVirtualMemorySize
[uint64]$TotalVisibleMemorySize
[string]$Version
[string]$BootDevice
[string]$BuildNumber
[string]$BuildType
[string]$CodeSet
[string]$CountryCode
[string]$CSDVersion
[bool]$DataExecutionPrevention_32BitApplications
[bool]$DataExecutionPrevention_Available
[bool]$DataExecutionPrevention_Drivers
[byte]$DataExecutionPrevention_SupportPolicy
[bool]$Debug
[uint32]$EncryptionLevel
[byte]$ForegroundApplicationBoost
[uint32]$LargeSystemCache
[string]$Locale
[string]$Manufacturer
[string[]]$MUILanguages
[uint32]$OperatingSystemSKU
[string]$Organization
[string]$OSArchitecture
[uint32]$OSLanguage
[uint32]$OSProductSuite
[bool]$PAEEnabled
[string]$PlusProductID
[string]$PlusVersionNumber
[bool]$PortableOperatingSystem
[bool]$Primary
[uint32]$ProductType
[string]$RegisteredUser
[string]$SerialNumber
[uint16]$ServicePackMajorVersion
[uint16]$ServicePackMinorVersion
[uint32]$SuiteMask
[string]$SystemDevice
[string]$SystemDirectory
[string]$SystemDrive
[string]$WindowsDirectory
[string]$PSComputerName
[Microsoft.Management.Infrastructure.CimClass]$CimClass
[Microsoft.Management.Infrastructure.Generic.CimKeyedCollection`1[[Microsoft.Management.Infrastructure.CimProperty, Microsoft.Management.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]]$CimInstanceProperties
[Microsoft.Management.Infrastructure.CimSystemProperties]$CimSystemProperties
}
I probably don't need all those properties and I'll handle that in a bit. But first let me try this with a different type of object and refine my technique.
$properties = "Name","Displayname","MachineName","StartType"
(Get-Service bits).psobject.properties |
Where-Object {$properties -contains $_.name} |
ForEach-Object -Begin {
$class = @"
class mySvc {
#properties
"@
} -Process { $class += "[{0}]`${1}`n" -f $_.TypeNameOfValue, $_.name } -End { $class += "}" }
And my resulting class:
class mySvc {
properties
[System.String]$Name
[System.String]$DisplayName
[System.String]$MachineName
[System.ServiceProcess.ServiceStartMode]$StartType
}
Convert-ObjectToClass
Now that I have working code from a prompt, i can wrap up in a function and add features.
Function Convert-ObjectToClass {
[cmdletbinding()]
[outputType([String])]
Param(
[Parameter(Position = 0, Mandatory, ValueFromPipeline)]
[object]$InputObject,
[Parameter(Mandatory, HelpMessage = "Enter the name of your new class")]
[string]$Name,
[string[]]$Properties,
[string[]]$Exclude
)
Begin {
Write-Verbose "Starting $($myinvocation.MyCommand)"
} #begin
Process {
Write-Verbose "Converting existing type $($InputObject.getType().Fullname)"
#create a list to hold properties
$prop = [system.collections.generic.list[object]]::new()
#define the class here-string
$myclass = @"
# this class is derived from $($InputObject.getType().Fullname)
class $Name {
#properties
"@
#get the required properties
if ($Properties) {
$InputObject.psobject.properties | Where-Object { $Properties -contains $_.name } |
Select-Object -Property Name, TypeNameOfValue |
ForEach-Object {
Write-Verbose "Adding $($_.name)"
$prop.Add($_)
} #foreaach
} #if Properties
else {
Write-Verbose "Adding all properties"
$InputObject.psobject.properties | Select-Object -Property Name, TypeNameOfValue |
ForEach-Object {
Write-Verbose "Adding $($_.name)"
$prop.Add($_)
} #foreach
} #else all
if ($Exclude) {
foreach ($item in $Exclude) {
Write-Verbose "Excluding $item"
#remove properties that are tagged as excluded from the list
[void]$prop.remove($($prop.where({ $_.name -like $item })))
}
}
Write-Verbose "Processing $($prop.count) properties"
foreach ($item in $prop) {
#add the property definition name to the class
#e.g. [string]$Name
$myclass += "[{0}]`${1}`n" -f $item.TypeNameOfValue, $item.name
}
#add placeholder content to the class definition
$myclass += @"
#Methods can be inserted here
<#
[returntype] MethodName(<parameters>) {
code
return value
}
#>
#constructor placeholder
$Name() {
#insert code here
}
} #close class definition
"@
#if running VS Code or the PowerShell ISE, copy the class to the clipboard
#this code could be modified to insert the class into the current document
if ($host.name -match "ISE|Visual Studio Code" ) {
$myClass | Set-Clipboard
$myClass
Write-Host "The class definition has been copied to the Windows clipboard." -ForegroundColor green
}
else {
$myClass
}
} #process
End {
Write-Verbose "Ending $($myinvocation.MyCommand)"
} #end
}
The function name is up for debate. The name I'm using is really a literal representation of what the function will accomplish. But "ObjectToClass" is a weird noun. A better name might be ConvertTo-PSClass. An argument could also be made to use a different verb such as Out or Export. These are design considerations not to be overlooked.
My function includes PowerShell code to detect if it is running in the PowerShell ISE or VS Code and if so, to copy the class to the clipboard. I am a little hesitant to include this in the function because this is a separate task from defining the class and PowerShell functions should do only one thing. But, this code doesn't add any pipelined output, and from my perspective, I'm running the function to create a class definition that I can use in a scripting project.
I can use it like this:
Get-Ciminstance win32_Operatingsystem | Convert-ObjectToClass -Properties Caption,CSName,Version,InstallDate -Name myOS
The class definition is far from perfect and I'm expecting to have to refine it.
this class is derived from Microsoft.Management.Infrastructure.CimInstance
class myOS {
properties
[string]$Caption
[CimInstance#DateTime]$InstallDate
[string]$CSName
[string]$Version
Methods can be inserted here
<# [returntype] MethodName(<parameters>) { code return value } #>
constructor placeholder
myOS() {
#insert code here
}
} #close class definition
For example, the InstallDate needs to be a [DateTime] object. I also know I'll need to rename some properties such as $CSName to $Computername. But my function got me started.
Since I've started, I might as well give you a taste of the entire process. Here's my revised class definition.
class myOS {
#properties
[string]$Name
[DateTime]$InstallDate
[string]$Computername = $env:COMPUTERNAME
[string]$Version
[datetime]$AuditDate = (Get-Date)
#Methods can be inserted here
hidden [timespan] GetInstallAge() {
$time = New-TimeSpan -Start $this.InstallDate -End (Get-Date)
return $time
}
#constructor placeholder
myOS([string]$Computername) {
$os = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $Computername
$this.name = $os.caption
$this.version = $os.Version
$this.installDate = $os.InstallDate
$this.Computername = $Computername
}
} #close class definition
Because I'm defining a custom object, I can also define type and format extensions, although I'm skipping the latter for now.
Update-TypeData -TypeName myOS -MemberType ScriptProperty -MemberName InstallAge -Value { $this.GetInstallAge() } -Force
The last thing I need is an easy way for a user to use the class. I don't want to force the user to have to use the raw class definition. Instead, create your own helper functions.
Function Get-OS {
[cmdletbinding()]
[Outputtype("myOS")]
Param([string[]]$Computername = $env:COMPUTERNAME)
foreach ($computer in $computername) {
New-Object -TypeName myOS -ArgumentList $computer
}
}
I have to admit, I might actually use this function. This is definitely not beginner-level PowerShell so if you have questions, please don't be shy and ask in the comments.
Excellent example. I don’t have any questions right now but wait until I try it 🙂