Skip to content

Instantly share code, notes, and snippets.

@Jaykul
Last active May 7, 2022 01:13
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Jaykul/f95a069bb190aef3649f6eddae200d1c to your computer and use it in GitHub Desktop.
Save Jaykul/f95a069bb190aef3649f6eddae200d1c to your computer and use it in GitHub Desktop.
Validating PowerShell class properties

The simplest way to enforce rules on PowerShell class properties is to set the Type of the property, of course. But sometimes you want something a little bit more clever than that. One solution is to use a Validate* attribute, like ValidateRange or ValidateLength or ValidateSet attribute...

However, you can write your own, by just deriving from ValidateArguments() or something that derives from that, like the ValidateEnumeratedArguments class allows you to validate a whole array of items.

For instance, a simple validator would be one that validates uniqueness, based on a specific property. Our validator will be created fresh each time it's used, and then each item in the array will be passed through ValidateElement, so this works:

using namespace System.Collections.Generic
using namespace System.Management.Automation
class ValidateUnique : ValidateEnumeratedArgumentsAttribute {

    [string]$PropertyName
    [HashSet]$Existing = [HashSet]::new()

    ValidateUnique([string]$PropertyName) {
       $this.PropertyName = $PropertyName
    }

    [void]ValidateElement($Element) {
        if(!$Existing.Add($Element.$PropertyName)) {
            throw "$Element is a duplicate"
        }
    }
}

Given that validator, you could make a class which has a Property that doesn't allow duplicates:

class TestClass {
    [ValidateUnique($null)]
    [int[]]$Unique
}
# Create one
$n = [TestClass]::new()

# you can set it
$n.Unique = 1,2,3
# repeatedly
$n.Unique = 1,2,3,4

# but if you add a dupe it throws
$n.Unique = 1,2,3,4,2

But if you just use simple equality, it won't work for complex things (like files), because they won't show up as equal. In that case, you need to specify a property to use to compare, like the full path name:

class TestClass {
    [ValidateUnique("FullName")]
    [IO.FileSystemInfo[]]$Unique
}

$n = [TestClass]::new()  
$n.Unique = Get-ChildItem
# This will throw, because it's adding the same items again:
$n.Unique += Get-ChildItem

It's worth pointing out that at fist I thought I could do this by just using a Hashset for the property. Since you can customize the equality comparison of hashsets, you could, write one that only looks at a specific property of PowerShell object:

Unique Type Constraint

using namespace System.Collections.Generic
class TypeEqualityComparer : IEqualityComparer[PSObject]
{

    [string]$PropertyName

    TypeEqualityComparer([string]$PropertyName)
    {
        $this.PropertyName = $PropertyName
    }
    
    [bool] Equals([PSObject]$first, [PSObject]$second)
    {
        return $first.($this.PropertyName) -eq $second.($this.PropertyName)
    }

    [int]GetHashCode([PSObject]$item)
    {
        return $item.($this.PropertyName).GetHashCode();
    }
}

class TestClass {
    [Hashset[PSObject]]$Unique = [Hashset[PSObject]]::new([TypeEqualityComparer]::new("FullName"))
}

The problem is, PowerShell doesn't have read-only properties, so I can't be sure someone won't just set the Unique property itself to a new value, perhaps even inadvertently:

$Test = [TestClass]::new()
Get-ChildItem | ForEach { $Test.Unique.Add($_) } 

# And if you call that again, it does not add them (no exception, it just returns $False
Get-ChildItem | ForEach { $Test.Unique.Add($_) }

# But if you just do something simple like this, it all goes sideways
$Test.Unique += Get-ChildItem
@saladproblems
Copy link

saladproblems commented Mar 6, 2020

Thanks for writing this, I'm still trying to parse out exactly how it works. I've got an SCCM module which handles a lot of different types of CIM instances, and it would really simplify my code if I could get a validator working that will verify ClassName of the CIM instances.

Your post is literally the only one I could find on this class, have you ever made something like this? I can't get it to work:

` class ValidateUnique : System.Management.Automation.ValidateEnumeratedArgumentsAttribute {

    [string]$ClassName   

    ValidateUnique([string]$PropertyName) {
    $this.ClassName = $ClassName
    }

    [void]ValidateElement($Element) {
        if($Element.CimClass.CimClassName -ne $ClassName) {
            throw "$($Element.CimClass.CimClassName) is not $($this.ClassName)"
        }
    }
}`

@Jaykul
Copy link
Author

Jaykul commented Mar 9, 2020

If I understand what you're saying, I think in this case you should use the ValidateArgumentsAttribute, because you're trying to validate a single value. You could make something that behaves a bit like the PSTypeNameAttribute like this:

class CimClassName : System.Management.Automation.ValidateArgumentsAttribute {

    [string]$ClassName
    [string]$NameSpace
 
    CimClassName([string]$ClassName) {
        $this.ClassName = $ClassName
    }

    [void]Validate([Object]$Element, [System.Management.Automation.EngineIntrinsics]$EngineIntrinsics) {
        if($Element.CimClass.CimClassName -ne $this.ClassName) {
            throw "$($Element.CimClass.CimClassName) is not a $($this.ClassName)"
        }
    }
}

That would allow you to write classes like this:

class TestClass {
    [CimClassName("Win32_Process")][CimInstance]$Process
}

Which would only accept a Win32_Process CimInstance, and not other CimInstance objects

image

@saladproblems
Copy link

saladproblems commented Mar 10, 2020 via email

@Stephanevg
Copy link

This is pretty cool indeed. Thanks for sharing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment