Powershell

Custom types with Powershell

Objects in Powershell do not necessarily have to have a defined type (or better, they can simply all be generic PSCustomObject) but from time to time it can be useful to use a custom type, for example to get nice TAB completion when passing those objects down the pipeline or to use custom formatting (more on this in a future post).

Custom types are relatively easy to add; in essence, once we have an object ready to be returned as function output we can simply insert the type name in the TypeNames collection

$outObj = [PSCustomObject]@{
    ServerName    = $resourceIdTokens[8];
    DatabaseName  = $resourceIdTokens[10];
    Sku           = $database.CurrentServiceOBjectiveName;
    CurrentSizeGb = ($dbSize.Data[0].Maximum / 1gb).ToString("0.00");
    MaxSizeGb     = $database.MaxSizeBytes / 1gb;
    '%-Used'      = (($dbSize.Data[0].Maximum) / ($database.MaxSizeBytes) * 100).ToString("0.00")
}
$outObj.PSObject.TypeNames.Insert(0, 'AzSqlDatabaseSize')
$outObj

Here I am retrieving some properties from an Azure Sql Database, add them to a PSCustomObject and call $outObj.PSObject.TypeNames.Insert to insert a custom type name (AzSqlDatabaseSize) at index zero of the TypeNames collection. The output of commands would look like this:

ServerName    : MySqlServer
DatabaseName  : MySqlDatabase
Sku           : P15
CurrentSizeGb : 3511.92083740234
MaxSizeGb     : 4096
%-Used        : 85.74

If I pipe the command to Get-Member instead, we can see the object type returned:

Get-AzSqlDatabaseSize -ResourceGroupName Default-SQL-CentralUS -ServerName MySqlServer -DatabaseName MySqlDatabase | gm

   TypeName: AzSqlDatabaseSize
Name          MemberType   Definition
----          ----------   ----------
Equals        Method       bool Equals(System.Object obj)
GetHashCode   Method       int GetHashCode()
GetType       Method       type GetType()
ToString      Method       string ToString()
%-Used        NoteProperty string %-Used=0.05
CurrentSizeGb NoteProperty string CurrentSizeGb=0.25
DatabaseName  NoteProperty string DatabaseName=MyDatabaseName
MaxSizeGb     NoteProperty long MaxSizeGb=500
ServerName    NoteProperty string ServerName=MySqlServer
Sku           NoteProperty string Sku=P15

Also, if I use this command in a pipeline I get TAB auto-completion: notice when I pipe the command to Where-Object (? is the shortcut) the property names suggested are exactly the ones returned by my custom type:

tab completion with custom object type
tab completion with custom object type

Powershell 5 added classes support and custom types is one of the supported (I’d say suggested?) scenarios:

– Define custom types in PowerShell using familiar object-oriented programming semantics like classes, properties, methods, inheritance, etc.
– Debug types using the PowerShell language.
– Generate and handle exceptions using formal mechanisms.
– Define DSC resources and their associated types by using the PowerShell language.

https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_classes?view=powershell-6

A sample function to use classes for this scope would look like this:

function Get-AzSqlDatabaseSize {
    begin {
        class AzSqlDatabaseSize {
            [string]$ServerName
            [string]$DatabaseName
            [string]$Sku
            [string]$CurrentSizeGb
            [string]$MaxSizeGb
            [string]$PercentageUsed

            AzSqlDatabaseSize($ServerName, $DatabaseName, $Sku, $CurrentSizeGb, $MaxSizeGb, $PercentageUsed) {
                $this.ServerName = $ServerName;
                $this.DatabaseName = $DatabaseName;
                $this.Sku = $Sku;
                $this.CurrentSizeGb = $CurrentSizeGb;
                $this.MaxSizeGb = $MaxSizeGb;
                $this.PercentageUsed = $PercentageUsed
            }
        }
    }

    process {

        [...]
    
        $outObj = New-Object -TypeName 'AzSqlDatabaseSize' -ArgumentList $ServerName, $DatabaseName, $Sku, $CurrentSizeGb, $MaxSizeGb, $PercentageUsed

        $outObj
    }
}

This is a bare-bone class, it accepts six parameters and does not have any properties or methods (I don’t need any for this demonstration) except for one constructor which I use simply assign those properties to the class. Line 35 instantiates the object that is returned to the caller on line 37. Once we have the class defined, how to create the object (how to instantiate the class I should say) follow the same Powershell rules as any other object; the block of code below shows three equivalent methods:

# PSCustomOBject hashtable (no class declaration required)
$outObj = [PSCustomObject]@{
    ServerName    = $ServerName;
    DatabaseName  = $DatabaseName;
    Sku           = $Sku;
    CurrentSizeGb = $CurrentSizeGb;
    MaxSizeGb     = $MaxSizeGb
    '%-Used'      = $PercentageUsed
}
$outObj.PSObject.TypeNames.Insert(0, 'AzSqlDatabaseSize')


# .NET syntax (class declaration required to define the AzSqlDatabaseSize type)
$outObj = [AzSqlDatabaseSize]::new($ServerName, $DatabaseName, $Sku, $CurrentSizeGb, $MaxSizeGb, $PercentageUsed)


# New-Object cmdlet
$outObj = New-Object -TypeName 'AzSqlDatabaseSize' -ArgumentList $ServerName, $DatabaseName, $Sku, $CurrentSizeGb, $MaxSizeGb, $PercentageUsed

This is all good but not enough though: Powershell requires that we register the new type in the current session before it can be used as shown above. This is where Types.ps1xml comes into play, or at least the logic it provides. The file is located in the $PSHome directory and it contains most of the type definition used by Powershell and its core modules (other modules add their own *.ps1xml types file). To create my types I usually look for an existing similar type and customize it for my needs. For example, this is the definition for the AzSqlDatabaseSize type I’ve shown above:

<Type>
  <Name>AzSqlDatabaseSize</Name>
  <Members>
    <NoteProperty>
      <Name>ServerName</Name>
      <Value>ServerName</Value>
    </NoteProperty>
    <NoteProperty>
      <Name>DatabaseName</Name>
      <Value>DatabaseName</Value>
    </NoteProperty>
    <NoteProperty>
      <Name>Sku</Name>
      <Value>Sku</Value>
    </NoteProperty>
    <NoteProperty>
      <Name>CurrentSizeGb</Name>
      <Value>CurrentSizeGb</Value>
    </NoteProperty>
    <NoteProperty>
      <Name>MaxSizeGb</Name>
      <Value>MaxSizeGb</Value>
    </NoteProperty>
    <NoteProperty>
      <Name>%-Used</Name>
      <Value>%-Used</Value>
    </NoteProperty>
  </Members>
</Type>

Each property has a Name and a Value pair: the Value name corresponds to the property from my output object and to keep things simple I use the same names across the three references (ServerName is the property name, its value and also the object property name in my Powershell function). This function is part of a module, so I saved this xml block in a .ps1xml file in the module folder (the file name is not really important at this stage) and add a reference to this file in the module Data File (psd1):

# Type files (.ps1xml) to be loaded when importing this module
TypesToProcess = @('LSEAzure.ps1xml')

When the module is loaded the custom type(s) are automatically added to the session.


I am enough of an artist to draw freely upon my imagination. Imagination is more important than knowledge. Knowledge is limited. Imagination encircles the world. – Albert Einstein

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.