Blog/content/posts/pstypename/index.md

6.7 KiB

title date categories tags
Parameter Validation with PSTypeName 2022-03-16T09:24:56+01:00
PowerShell
parameter
validation
PSTypeName

ship

🖼️ Intro

This post explains how to use PSCustomObjects as function parameters. We compare the basic usage with an advanced one using the [PSTypeName()] parameter attribute.

🗑️ Well-Known Workflow

So let's start with a common object definition how it is used with a function:

$Rocinante = [PSCustomObject]@{
  Owner = 'Martian Congressional Republic Navy'
  Type = 'Light Frigate'
  Class = 'Corvette'
  Registry = 'ECF-270'
  HullNumber = '158'
  LengthInMeter = 46
  Name = 'Rocinante'
}

As you can see, a PSCustomObject has still the the same class type and just differs by its note properties.

> $Rocinante | Get-Member

   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()
Class          NoteProperty  string Class=Corvette
HullNumber     NoteProperty  string HullNumber=158
LengthInMeter  NoteProperty  int LengthInMeter=46
Name           NoteProperty  string Name=Rocinante
Owner          NoteProperty  string Owner=Martian Congressional Republic Navy
Registry       NoteProperty  string Registry=ECF-270
Type           NoteProperty  string Type=Light Frigate

> $Rocinante.PSObject.TypeNames
System.Management.Automation.PSCustomObject
System.Object

So we can use the out object as an function parameter.

function Invoke-Launch {
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [PSCustomObject]$Ship
  )

  begin {}

  process {
    # Manual input validation for $Ship
    # test if all needed properties are present.

    $DockLength = 50
    if ($Ship.LengthInMeter -gt $DockLength) {
      Write-Error -Message "Ship doesn't fit in the docking station." -ErrorAction 'Stop'
    }
    # ...
    # ...
  }

  end {}
}

This common pattern could fail whenever someone changes your object properties. If the LengthInMeter property is missing you ran into an error. E.g.:

> $Rocinante = [PSCustomObject]@{ foo = 'bar' }
> Invoke-Launch -Ship $Rocinante

{{< note >}} Keep in mind - Because we are using here custom objects and not class instances, we can not use Rocinante as a parameter type like [Rocinante]$Ship which would solve this immediately. {{< /note >}}

To fix this we can use the [PSTypeName()] parameter attribute, to ensure an object with the correct type name is used. This doesn't verify your parameters but minimize the risk for using invalid parameter objects.

🛡️ PSTypeName Usage

Let's first modify the object creation and use a custom type name.

$Rocinante = [PSCustomObject]@{
  # You can use special property 'PSTypeName'
  # to set it implicit within the creation.
  PSTypeName = 'Ship.Corvette.LightFrigate'
  Owner = 'Martian Congressional Republic Navy'
  Type = 'Light Frigate'
  Class = 'Corvette'
  Registry = 'ECF-270'
  HullNumber = '158'
  LengthInMeter = 46
  Name = 'Rocinante'
}
# Legacy syntax for injection a custom type name
# $Rocinante.PSObject.TypeNames.insert(0,'Ship.Corvette.LightFrigate')
> $Rocinante | Get-Member

   TypeName: Ship.Corvette.LightFrigate

Name           MemberType    Definition
----           ----------    ----------
Equals         Method        bool Equals(System.Object obj)
GetHashCode    Method        int GetHashCode()
GetType        Method        type GetType()
ToString       Method        string ToString()
Class          NoteProperty  string Class=Corvette
HullNumber     NoteProperty  string HullNumber=158
LengthInMeter  NoteProperty  int LengthInMeter=46
Name           NoteProperty  string Name=Rocinante
Owner          NoteProperty  string Owner=Martian Congressional Republic Navy
Registry       NoteProperty  string Registry=ECF-270
Type           NoteProperty  string Type=Light Frigate

> $Rocinante.PSObject.TypeNames
Ship.Corvette.LightFrigate
System.Management.Automation.PSCustomObject
System.Object

Now we can replace the [PSCustomObject] parameter type by [PSTypeName()]

function Invoke-Launch {
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [PSTypeName('Ship.Corvette.LightFrigate')]$Ship
  )

  begin {}

  process {
    $DockLength = 50
    if ($Ship.LengthInMeter -gt $DockLength) {
      Write-Error -Message "Ship doesn't fit in the docking station." -ErrorAction 'Stop'
    }
    # ...
    # ...
  }

  end {}
}

💭 Final Thoughts

Over time, your PowerShell functions become more and more complex. You will reach a point where you start using objects as parameters. This is where the PSTypeName parameter attribute shown can help you.

In my experience, the ability to create custom classes (introduced in PowerShell 5) is rarely used for this.

Most PowerShell users I know have a SysOp or DevOps background. Few come from software development and try to use OOP paradigms and patterns.

Therefore I would also avoid using complex classes, especially if they use not only properties but also methods.

Like already mentioned PSTypeName just tests the used type name and not your definition details. You should consider creating a your objects within a wrapper function to mimic a class constructor:

function New-LightFrigate {
  [CmdletBinding()]
  [OutputType('Ship.Corvette.LightFrigate')]
  param (
    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [string]$Registry,

    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [string]$HullNumber,

    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [string]$Name
  )

  begin {}

  process {
    $Ship = [PSCustomObject]@{
      PSTypeName = 'Ship.Corvette.LightFrigate'
      Owner = 'Martian Congressional Republic Navy'
      Type = 'Light Frigate'
      Class = 'Corvette'
      Registry = $Registry
      HullNumber = $HullNumber
      LengthInMeter = 46
      Name = $Name
    }
    Write-Output $Ship
  }

  end {}
}

$Rocinante = New-LightFrigate -Name 'Rocinante' -Registry 'DE-MB2' -HullNumber '158'