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:
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
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 {