Today (April 6, 2016) I was pleased to present “High Class PowerShell” to the SQL PASS PowerShell Virtual User Group. In it I covered the creation of custom objects and classes in PowerShell.
The first question everyone asks is “where’s the code”. I’ve pasted the code samples for the main demo at the bottom of this post. You can review it here, or copy – paste into your own ISE.
The closing code on calling classes created in PowerShell was discussed in these two blog posts, so I won’t repeat it here.
- Accessing a PowerShell Class Defined In A Module From Outside A Module
- Resolving Ambiguous Class Names Across PowerShell Modules
During the presentation, I mentioned this code is a small subset of my larger Pluralsight course, Beginning PowerShell Scripting for Developers. You’ll find it and my other courses here:
https://www.pluralsight.com/authors/robert-cain
Don’t have a Pluralsight subscription? No problem, Pluralsight has graciously provided me some trial codes I can share with you. These will allow you a one month, no obligation trial with which you can watch any of my courses, or indeed any course found on Pluralsight. Just email free@arcanetc.com to request your code. (If you didn’t get to see the presentation, no worries, you can still email us for the Pluralsight trial code!)
When the video of the presentation goes live I will have a follow up blog post, meanwhile, and without further ado, here is the code sample.
<#----------------------------------------------------------------------------- High Class PowerShell Author: Robert C. Cain | @ArcaneCode | arcanecode@gmail.com http://arcanecode.com This module is Copyright (c) 2016 Robert C. Cain. All rights reserved. The code herein is for demonstration purposes. No warranty or guarentee is implied or expressly granted. This module may not be reproduced in whole or in part without the express written consent of the author. -----------------------------------------------------------------------------#> #-----------------------------------------------------------------------------# # Demo 0 -- Object Oriented Terminology #-----------------------------------------------------------------------------# <# class = blueprints properties = describe methods (functions) = actions object = house instantiating a new object = building a new house each object is an instance of the class = each house is a copy of the blueprint #> #-----------------------------------------------------------------------------# # Demo 1 -- Create a new object # This is the most common method of creating objects #-----------------------------------------------------------------------------# # The most basic way to create a custom object is to build a hash table with # the properties. Using [ordered] will ensure the properties remain in the # desired order. $properties = [ordered]@{ Schema = $Schema Table = $Table Comment = $Comment } # Start by creating an object of type PSObject $object = New-Object –TypeName PSObject -Property $properties # Now you can access it's properties $object.Schema = 'MySchema' $object.Table = 'MyTable' $object.Comment = 'MyComment' $object # Much of the time, the process of creating a custom object is wrapped in a # function to make it easier. function Create-Object ($Schema, $Table, $Comment) { # Build a hash table with the properties $properties = [ordered]@{ Schema = $Schema Table = $Table Comment = $Comment } # Start by creating an object of type PSObject $object = New-Object –TypeName PSObject -Property $properties # Return the newly created object return $object } $myObject = Create-Object -Schema "MySchema" -Table "MyTable" -Comment "MyComment" $myObject # Display in text. If you try to reference a property in text, it doesn't # work quite right. "My Schema = $myObject.Schema" # Instead, wrap it in $() to access a property. This will force PowerShell to # evaluate the object.property call as an expression, and return that to the # string "My Schema = $($myObject.Schema)" # Changing properties is easy $myObject.Schema = "New Schema" $myObject.Comment = "New Comment" $myObject #-----------------------------------------------------------------------------# # Demo 2 -- Create a new object by adding properties one at a time # In the previous demo a property hash table was used to generate the object # Behind the scenes it does the equivalent of what this function does #-----------------------------------------------------------------------------# function Create-Object ($Schema, $Table, $Comment) { # Start by creating an object of type PSObject $object = New-Object –TypeName PSObject # Add-Member by passing in input object Add-Member -InputObject $object ` –MemberType NoteProperty ` –Name Schema ` –Value $Schema # Alternate syntax, pipe the object as an input to Add-Member $object | Add-Member –MemberType NoteProperty ` –Name Table ` –Value $Table $object | Add-Member -MemberType NoteProperty ` -Name Comment ` -Value $Comment return $object } $myObject = Create-Object -Schema "MySchema" -Table "MyTable" -Comment "MyComment" $myObject # No difference in the way we display values or assign them "My Schema = $($myObject.Schema)" $myObject.Schema = "New Schema" $myObject # Show this is a PSCustomObject $myObject.GetType() # Each property has it's own .NET Data Type. # Note this is different from the Member Type $myObject.Comment.GetType() # To see the Member Types, pipe to Get-Member $myObject | Get-Member #-----------------------------------------------------------------------------# # Demo 3 -- Add alias for one of the properties #-----------------------------------------------------------------------------# Clear-Host # Demo 3 -- Add alias so both Comment and Description reference the same thing Add-Member -InputObject $myObject ` -MemberType AliasProperty ` -Name 'Description' ` -Value 'Comment' ` -PassThru # Use passthru to see the new object "Comment......: $($myObject.Comment)" "Description..: $($myObject.Description)" # Change the value $myObject.Description = 'This is now a description' "Comment......: $($myObject.Comment)" "Description..: $($myObject.Description)" # Demo 3 -- Add script block to object Clear-Host $block = { $fqn = $this.Schema + '.' + $this.Table return $fqn } Add-Member -InputObject $myObject ` -MemberType ScriptMethod ` -Name 'FullyQualifiedName' ` -Value $block ` -PassThru # Using Get-Member we can see the newly added script method $myObject | Get-Member # When calling our new function, parens are very important, without them # PowerShell will just display the function $myObject.FullyQualifiedName() #-----------------------------------------------------------------------------# # Demo 4 -- Script block with parameters #-----------------------------------------------------------------------------# Clear-Host $block = { param ($DatabaseName) $fqn = "$DatabaseName.$($this.Schema).$($this.Table)" return $fqn } Add-Member -InputObject $myObject ` -MemberType ScriptMethod ` -Name 'DatabaseQualifiedName' ` -Value $block # Place any parameters within the parens $myObject.DatabaseQualifiedName('MyDBName') #-----------------------------------------------------------------------------# # Demo 5 -- Script Property #-----------------------------------------------------------------------------# # These are analogues to properties in C#, with a Getter and Setter function Clear-Host # Add a property we can work with Add-Member -InputObject $myObject ` –MemberType NoteProperty ` –Name AuthorName ` –Value 'No Author Name' $myObject # Show the added property # This defines the GET for this property $getBlock = { return $this.AuthorName } # This defines the SET. Adding a simple check for the name $setBlock = { param ( [string]$author ) if($author.Length -eq 0) { $author = 'Robert C. Cain, MVP' } $this.AuthorName = $author } # Now add the custom Get/Set ScriptProperty to the member Add-Member -InputObject $myObject ` -MemberType ScriptProperty ` -Name Author ` -Value $getBlock ` -SecondValue $setBlock # Demo its use when passing as value $myObject.Author = 'ArcaneCode' "`$myObject.Author now equals $($myObject.Author )" # Now pass in nothing to see the setter functionality kicking in $myObject.Author = '' $myObject.Author # Unfortunately the original property is still available, and thus # the custom get/set can be bypassed $myObject.AuthorName = '' $myObject.Author # Author reflects value of AuthorName $myObject.Author = '' # Going thru scriptproperty sets correctly $myObject.Author #-----------------------------------------------------------------------------# # Demo 6 -- Set default properties # Note: Thanks to Poshoholic for his cool code sample, see it at: # http://poshoholic.com/2008/07/05/essential-powershell-define-default-properties-for-custom-objects/ #-----------------------------------------------------------------------------# Clear-Host # When just running the object, it displays all properties $myObject # If you have a lot, this can get overwhelming. Instead you can define a # default set to display. # Define the property names in an array $defaultProperties = 'Schema', 'Table', 'Comment', 'Author' # Create a property set object and pass in the array $defaultPropertiesSet ` = New-Object System.Management.Automation.PSPropertySet(` ‘DefaultDisplayPropertySet’ ` ,[string[]]$defaultProperties ` ) # Create a PS Member Info object from the previous property set object $members ` = [System.Management.Automation.PSMemberInfo[]]@($defaultPropertiesSet) # Now add to the object $myObject | Add-Member MemberSet PSStandardMembers $members # Now the object will just display the default list in standard output $myObject # Little easier to read in a list $myObject | Format-List # To display all properties, pipe through format-list with wild card for property $myObject | Format-List -Property * #-----------------------------------------------------------------------------# # Demo 7 - Create a class from .Net Code and call a static method #-----------------------------------------------------------------------------# # Load the contents of the file into a variable $code = @" using System; public class MyObjectStatic { public static string composeFullName(string pSchema, string pTable) { string retVal = ""; // Using retVal for Write-Verbose purposes retVal = pSchema + "." + pTable; return retVal; } // public static void composeFullName } // class MyObjectStatic "@ # Add a new type definition based on the code Add-Type -TypeDefinition $code ` -Language CSharpVersion3 # Call the static method of the object $mySchema = "dbo" $myTable = "ArcaneCode" $result = [MyObjectStatic]::composeFullName($mySchema, $myTable) $result #-----------------------------------------------------------------------------# # Demo 8 - Instantiate an object from .Net Code in embedded code #-----------------------------------------------------------------------------# $code = @" using System; public class MyObjectEmbedded { public string SomeStringName; public string composeFullName(string pSchema, string pTable) { string retVal = ""; // Using retVal for Write-Verbose purposes retVal = pSchema + "." + pTable; return retVal; } // public string composeFullName } // class MyObjectEmbedded "@ # Add a new type definition based on the code Add-Type -TypeDefinition $code ` -Language CSharpVersion3 # Instantiate a new version of the object $result = New-Object -TypeName MyObjectEmbedded # Set and display the property $result.SomeStringName = "Temp" $result.SomeStringName # Call the method $result.composeFullName($mySchema, $myTable) #-----------------------------------------------------------------------------# # Demo 9 - Create object from .Net Code in an external file #-----------------------------------------------------------------------------# # Path and File Name $file = 'C:\PS\Classes and Modules\HighClassPowerShell.cs' # Open the file in the ise to look at it psedit $file # Load the contents of the file into a variable $code = Get-Content $file | Out-String # Add a new type definition based on the code Add-Type -TypeDefinition $code ` -Language CSharpVersion3 # Call the static method of the object $mySchema = "dbo" $myTable = "ArcaneCode" $result = [MyObjectExternal]::composeFullName($mySchema, $myTable) $result #-----------------------------------------------------------------------------# # Demo 10 - Add to an existing object #-----------------------------------------------------------------------------# # Going to add a script method and note property to the System.IO.FileInfo # objects returned by Get-ChildItem # Define the custom script method $script = { $retValue = 'Unknown' switch ($this.Extension) { '.ps1' { $retValue = 'Script' } '.psm1' { $retValue = 'Module' } '.psd1' { $retValue = 'Module Declration' } '.cs' { $retValue = 'C# File'} default { $retValue = 'No Clue. Seriously stumped here.'} } return $retValue } # Load a variable with a collection of file objects Set-Location 'C:\PS\Classes and Modules' $items = Get-ChildItem $itemCount = 0 foreach($item in $items) { # Add a note property, setting it to the current item counter $itemCount++ $item | Add-Member –MemberType NoteProperty ` –Name ItemNumber ` –Value $itemCount # Add script property to the individual file object Add-Member -InputObject $item ` -MemberType ScriptMethod ` -Name 'ScriptType' ` -Value $script "$($item.ItemNumber): $($item.Name) = $($item.ScriptType())" } # Show our new note and script having been added to the FileInfo type $items[0] | Get-Member #-----------------------------------------------------------------------------# # Demo 11 - Serializing an Object #-----------------------------------------------------------------------------# # Create a simple object $mySiteProperties = [ordered]@{ WebSite = 'ArcaneCode' URL = 'http://arcanecode.com' Twitter = '@ArcaneCode' } # Convert it to an object $mySite = New-Object –TypeName PSObject -Property $mySiteProperties # Show the object $mySite # Save the object to a file and $savedDataFile = 'C:\PS\Classes and Modules\mySite.xml' $mySite | Export-Clixml $savedDataFile psedit $savedDataFile # Now grab the saved object and recreate in a different variable $newMySite = Import-Clixml $savedDataFile $newMySite #region Enum #-----------------------------------------------------------------------------# # Show Enums in PS #-----------------------------------------------------------------------------# # Define the valid values for the enum Enum MyTwitters { ArcaneTC ArcaneCode N4IXT } # Note when typing the last : will trigger intellisense! $tweet = [MyTwitters]::ArcaneCode $tweet # See if they picked something valid [enum]::IsDefined(([MyTwitters]), $tweet) # Set it to something invalid and see if it passes as an enum $tweet = 'Invalid' [enum]::IsDefined(([MyTwitters]), $tweet) #endregion Enum #region Basic Class #-----------------------------------------------------------------------------# # Basic Class #-----------------------------------------------------------------------------# Class Twitterer { # Create a property [string]$TwitterHandle # Create a property and set a default value [string]$Name = 'Robert C. Cain' # Function that returns a string [string] TwitterURL() { $url = "http://twitter.com/$($this.TwitterHandle)" return $url } # Function that has no return value [void] OpenTwitter() { Start-Process $this.TwitterURL() } } $twit = [Twitterer]::new() $twit.GetType() $twit.TwitterHandle = 'ArcaneCode' $twit.TwitterHandle # See default property value $twit.Name # Override default value $twit.Name = 'Robert Cain' $twit.Name $myTwitter = $twit.TwitterURL() $myTwitter $twit.OpenTwitter() #endregion Basic Class #region Advanced Class #-----------------------------------------------------------------------------# # Advanced Class # Constructors # Overloaded Methods #-----------------------------------------------------------------------------# Class TwittererRedux { # Default Constructor TwittererRedux () { } # Constructor passing in Twitter Handle TwittererRedux ([string]$TwitterHandle) { $this.TwitterHandle = $TwitterHandle } # Constructor passing in Twitter Handle and Name TwittererRedux ([string]$TwitterHandle, [string]$Name) { $this.TwitterHandle = $TwitterHandle $this.Name = $Name } # Create a property [string]$TwitterHandle # Create a property and set a default value [string]$Name = 'Robert C. Cain' # Static Properties static [string] $Version = "2016.04.06.001" # Function that returns a string [string] TwitterURL() { $url = "http://twitter.com/$($this.TwitterHandle)" return $url } # Overloaded Function that returns a string [string] TwitterURL($twitterHandle) { $url = "http://twitter.com/$($twitterHandle)" return $url } # Function that has no return value [void] OpenTwitter() { Start-Process $this.TwitterURL() } # Can launch a twitter page without instantiating the class static [void] OpenTwitterPage([string] $TwitterHandle) { $url = "http://twitter.com/$($TwitterHandle)" Start-Process $url } } # Create a class using default constructor $twitDefault = [TwittererRedux]::new() # Display without assigning "TwitterHandle = $($twitDefault.TwitterHandle)" # Now assign and display again $twitDefault.TwitterHandle = 'ArcaneTC' "TwitterHandle = $($twitDefault.TwitterHandle)" # Show version one of TwitterURL "URL = $($twitDefault.TwitterURL())" # Show overloaded version "URL = $($twitDefault.TwitterURL('ArcaneCode'))" # Create a new instance using the second constructor $twitAdvanced = [TwittererRedux]::new('N4IXT') # Display without assigning - Should have what was passed in constructor "TwitterHandle = $($twitAdvanced.TwitterHandle)" # Create yet another instance using the third constructor $twitAdvanced2 = [TwittererRedux]::new('ArcaneCode', 'R Cain') $twitAdvanced2.TwitterHandle $twitAdvanced2.Name # Static Value - Can be called without initializing the class [TwittererRedux]::Version # Use the static method [TwittererRedux]::OpenTwitterPage('ArcaneTC') #endregion Advanced Class