Custom PowerShell Attributes

powershell header

I was looking at the source of a cool module by Joel Sallow (vexx32 / vexx32) called PSKoans when I noticed an attribute I had never seen before [Koan(Position = 400)] at the top of a number of files in the project. Obviously a custom attribute I was intrigued and needed to know more.

Attributes in PowerShell

The attributes we’re talking about in this context are things like [cmdletbinding()], [ValidatePattern()], [parameter()] and so on that allow you to attach additional information to a class, function, or property. In some cases their mere presence is all that’s required ([ValidateNotNullorEmpty()]) and at other times they expose additional properties that can be set ([parameter(Mandatory=$true)]).

Creating an Attribute

Creating your own attributes in PowerShell is as simple inheriting from the System.Attribute class. (Note: it is best practice to include the word ‘Attribute’ in the name.)

class MyExampleAttribute : attribute {
}

Like any other class in PowerShell your attribute can have properties and methods.

class MyExampleAttribute : attribute {
    # This is a property
    [String]$String

    #This is a constructor
    MyExampleAttribute ([string]$String) {
        $this.String = $String
    }
}

Our new attribute can now be used like any other: [MyExample(String='Hello World')] or simply [MyExample('Hello World')]. Here’s an example of attaching it to a function and retrieving it: (Note: You use the name without ‘Attribute’ at the end but still need to include it when fetching.)

function Get-Foo {
    [MyExample('Hello World')]
    param()

    return 'Foo'
}

$Funct = Get-Command Get-Foo
$Funct.ScriptBlock.Attributes | Where-Object { $_.TypeID.Name -eq 'MyExampleAttribute' }

This will return an object with our String property and the TypeId:

String      TypeId            
------      ------            
Hello World MyExampleAttribute

If all we wanted was the string we could do something like this:

$Funct = Get-Command Get-Foo
$MyString = ($Funct.ScriptBlock.Attributes | Where-Object { $_.TypeID.Name -eq 'MyExampleAttribute' }).String
$MyString

Putting it to Use

At this point you might be asking what practical use could this possibly serve. Kevin Marquette ( KevinMarquette) has a great guide on writing your own validators which is worth a read but for my own part I used this new knowledge to add meta-data to Pester tests in my Check-It project.

What Check-It does is run Pester tests and send the results in an email or Microsoft Teams notification. I wanted to be able to schedule a single script file that would run through as many different Pester test files as there were in a test directory but allow the results of different tests to be sent to different recipients; Enter custom attributes! I created two attributes [CINotifications()] and [CITeamsNotifications()] which let you specify where to send the results per-file.

When scheduling Check-It you create a task that runs RunChecks.ps1 periodically, it will pick up all of the files containing Pester tests in its check directories, and for each file it gets the data from the custom attributes if they’re present to direct the notifications for just that file. For example:

# TestChecks.ps1
[CINotifications('admins@example.com',1)]
param()

Describe "My Pester Tests" {
    Context "Hello World" {
        It "is obviously broken" {
            $false | should -be $true
        }
    }
}

When Check-It runs the tests above it will see the CINotifications attribute and send the results to admins@example.com. (The 1 specifies that it should send both failures and successes rather than just failures.) The custom attributes makes it easy to drop in new test files and with different recipients.