Argument Transformation Attributes¶
Introduction¶
Argument transformation attributes make it possible to offer your users
some flexibility in how they supply values for parameters. I've started to use
these in the MilestonePSTools module to
make it possible to provide a name instead of a strongly typed object like a
[RecordingServer]
or a [Role]
, while still making it clear in the
Get-Help
documentation what the expected object type is, and without
polluting functions with object transformation code.
In my last post I introduced argument completers, and I consider these an absolute necessity for PowerShell functions and modules that will be shared and used by many people. Argument transformation attributes on the other hand are an advanced feature that can look intimidating to those early in their PowerShell journey. They are, in my opinion, syntactic sugar and thus purely optional.
Example use case¶
It has been almost 4 years since the first release of he MilestonePSTools
module. In that time, one common question has been about ParameterBindingException
errors. For example, if you wanted to get all hardware (cameras) from a
recording server, you might try Get-VmsHardware -RecordingServer 'Docker Recorder'
which seems perfectly reasonable. But you'd be met with the following error...
Cannot bind parameter 'RecordingServer'. Cannot convert the "Docker Recorder" value of type "System.String" to type "VideoOS.Platform.ConfigurationItems.RecordingServer".
The Get-VmsHardware
function expects a recording server object instead of
a recording server name. The correct usage would then look like...
$recorder = Get-VmsRecordingServer -Name 'Docker Recorder'
# Pipe the recording server in to Get-VmsHardware
$recorder | Get-VmsHardware
# Or provide it as a named parameter
Get-VmsHardware -RecordingServer $recorder
By introducing an argument transformation attribute, we can make the
Get-VmsHardware
function accept either a recording server
object, or a recording server name, without changing the parameter type or
any of the code within the begin
, process
, or end
blocks.
Writing an argument transformation attribute¶
An argument transformation attribute must be written as a class
and inherit from the System.Management.Automation.ArgumentTransformationAttribute
class. Your class must then override the Transform(EngineIntrinsics, Object)
abstract method which is where the code that performs the object
transformation will go.
Below you will find my RecorderNameTransformAttribute
implementation. I only
want it to transform strings into recording server objects. If the value
provided by the user is $null
or is not a string, then the object
will be returned as-is. I could do additional checking during the argument
transformation, but PowerShell's own parameter binding and handling of null or
invalid types is already so good. Why reinvent the wheel?
Using the argument transform in Get-VmsHardware¶
Once the argument transformation attribute class has been defined, it can be
used in any cmdlet or function in your script or module by adding [RecorderNameTransformAttribute()]
(or whatever you decide to call your custom
attribute) between [Parameter()]
and the parameter name. In the
definition of Get-VmsHardware
below, the highlighted line was
the only change required for the function to accept recording servers by name.
- This attribute is the only change required to the
Get-VmsHardware
function to enable it to accept recording server names in addition to[RecordingServer]
objects. - By adding this attribute we can be sure that all elements in
$RecordingServer
have a value and are not$null
inside theprocess {}
block.
Adding an argument completer¶
While I hold the opinion that argument transformation attributes are nearly
always "extra" and not required, now that it's been implemented for Get-VmsHardware
we should probably make it easy to take advantage of using an argument completer.
By adding the argument completer below to the class and function definitions
below, the user will be able to tab or list-complete values for the
-RecordingServer
parameter, making it not only possible to provide a name
instead of an object, but easy!
The final result¶
We can now put the three code blocks above together and use it! It's important
to note though that when you're working with PowerShell classes like the
RecorderNameTransformAttribute
class in this example, we can't reference
the class before defining the class.
What I mean by this is that we can't add the [RecorderNameTransformAttribute()]
attribute to a function parameter if the class definition appears somewhere
after, or below the Get-VmsHardware
function definition. So if you copy &
paste the script below as-is, it will work just fine. But if you move the class
definition down under the function, PowerShell will complain that it doesn't
recognize the RecorderNameTransformAttribute
class when it attempts to process
the [RecorderNameTransformAttribute()]
attribute.
When you use classes in a module, it's important to dot source the file(s) where your class(es) are defined before you dot source your functions. And if you have everything in a single .PSM1 file, put your classes at the top of the file so that they are always available when used in your functions.
The argument completer on the other hand can be defined anywhere, any time, because PowerShell doesn't attempt to invoke the argument completer script block until you have typed the associated command and parameter.
- This attribute is the only change required to the
Get-VmsHardware
function to enable it to accept recording server names in addition to[RecordingServer]
objects. - By adding this attribute we can be sure that all elements in
$RecordingServer
have a value and are not$null
inside theprocess {}
block.