Proxy Functions for Cmdlets with Dynamic Parameters

While working on an update for Pester today, I had to tackle an interesting problem. Pester’s mocking framework was not working well for commands that use dynamic parameters. What Pester essentially does, when you mock a function or Cmdlet, is create a new function with an identical param block to the one that’s being mocked.

When mocking an advanced function, this was a relatively easy problem to solve: just copy the original function’s DynamicParam block into the new one, and you’re done. However, I had to come up with another approach for mocking Cmdlets that have dynamic parameters. You might be thinking: “That’s nice. What does this have to do with Proxy Functions?” Well, when Pester mocks a Cmdlet, it’s creating a function with an identical param block. In that regard, it’s just like a proxy function, except it never calls the original Cmdlet.

The only hit I found on the internet when I searched for “Proxy Function” and “DynamicParam” was a Hey, Scripting Guy! guest article by Rohn Edwards. In that article, Rohn shows an example of creating a proxy function for Get-ChildItem, using an interesting approach of reading the output from Get-Command (with the -ArgumentList parameter) to find the dynamic parameters based on Path. I could probably have used that approach for every stock Provider-based Cmdlet, but it wouldn’t quite cut it for general use. Some Cmdlets might have dynamic parameter behavior that depends on parameters which aren’t passed by position, for example.

Instead, I decided to look for a way to get information about the dynamic parameters the same way the PowerShell engine does. Cmdlets which have dynamic parameters do so by implementing the IDynamicParameters interface, which has one method: GetDynamicParameters(). In theory, we should be able to create an instance of the Cmdlet’s type, set the properties associated with the static parameters, then call the GetDynamicParameters() method. With one small exception, that’s exactly how to get it working. The exception is that when you create an instance of a Cmdlet’s class, I couldn’t find a public way to assign its Execution Context, and that turned out to be necessary in order to call GetDynamicParameters() on many Cmdlets. I had to dip into Reflection slightly to assign a value to the Cmdlet’s private Context property:

function Get-DynamicParametersForCmdlet
{
    param (
        [string] $CmdletName,
        [hashtable] $Parameters
    )

    $command = Get-Command -Name $CmdletName -CommandType Cmdlet -ErrorAction Stop

    $cmdlet = New-Object $command.ImplementingType.FullName
    if ($cmdlet -isnot [System.Management.Automation.IDynamicParameters])
    {
        return
    }

    $flags = [System.Reflection.BindingFlags]'Instance, Nonpublic'
    $context = $ExecutionContext.GetType().GetField('_context', $flags).GetValue($ExecutionContext)
    [System.Management.Automation.Cmdlet].GetProperty('Context', $flags).SetValue($cmdlet, $context, $null)

    foreach ($keyValuePair in $Parameters.GetEnumerator())
    {
        $property = $cmdlet.GetType().GetProperty($keyValuePair.Key)
        if ($null -eq $property -or -not $property.CanWrite) { continue }

        $isParameter = [bool]($property.GetCustomAttributes([System.Management.Automation.ParameterAttribute], $true))
        if (-not $isParameter) { continue }

        $cmdlet.$($keyValuePair.Key) = $keyValuePair.Value
    }

    $cmdlet.GetDynamicParameters()
}

The version of this function in the Pester module has some extra error handling code to make it more robust; I trimmed down this snippet to make the basic points clear. Those are: Use Get-Command to find the name of the class that implements the Cmdlet. Use New-Object to create an instance of that class. Check to see if it implements IDynamicParameters. If so, set the Cmdlet object’s Context using Reflection, enumerate through the already-bound static parameters and set those properties on the Cmdlet object, then call GetDynamicParameters(). A call to this function is the only line in the DynamicParam block of the mock or Proxy function. For example:

DynamicParam
{
    Get-DynamicParametersForCmdlet -CmdletName Get-ChildItem -Parameters $PSBoundParameters
}

About Dave Wyatt

Microsoft MVP (PowerShell), IT professional.
This entry was posted in PowerShell and tagged , , . Bookmark the permalink.

4 Responses to Proxy Functions for Cmdlets with Dynamic Parameters

  1. Derp McDerp says:

    > When mocking an advanced function, this was a relatively easy problem to solve: just copy the original function’s DynamicParam block into the new one, and you’re done

    That doesn’t work if you have $script:scope_variables

    Like

    • Dave Wyatt says:

      Hmm, that’s a good point. I’ll look into that scenario soon.

      Like

    • Dave Wyatt says:

      That turned out to be a bit of a headache, but I got it sorted out in Pester by binding a script block to the same module where the mocked function was originally defined, with the script block containing the statements from the mocked function’s DynamicParam block. That way it can still resolve all the same script-scope variables when that code executes.

      The DynamicParam block in the mock is now just a stub that invokes that new script block in another scope.

      Like

  2. Pingback: Proxy Functions for Cmdlets with Dynamic Parameters | PowerShell.org

Leave a reply to Dave Wyatt Cancel reply