2

I am writing a PowerShell module, the functions inside this module have some parameters which will be re-used across all functions. Rather than copy-pasting the function definition each time I add a new function, I would like to define them at the top like a script variable and then insert them into each function, giving me a single place to update if they need to be changed.

Looking at how dynamic parameters are defined it seems like I should be able to define an object of that type and then reference it in the function definitions, but I can't find anything online giving me the correct syntax to do this.

Using PowerShell version 7.2

$Script:ReUsedParameters = param(
    [Parameter()]
    [String]$Name,
    [Parameter()]
    [Int]$Id
)


Function New-Command {
    Param ($ReUsedParameters)

    Write-Output "Name: $Name, ID: $ID"
}
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
David
  • 21
  • 2

2 Answers2

2

For the sake of answering, you can store the runtime parameters definitions in a script block and then call it & inside the function's dynamicparam block.

I do not think this is a good idea nor I recommend using this. All functions should have their own repeated param blocks if needed.

$reusedParameters = {
    $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()

    # Since both parameters don't have any arguments (Mandatory, Position, ValueFromPipeline, etc..)
    # you can use this one for both, otherwise, each dynamic parameter should have their own
    # Parameter Declaration
    [Parameter[]] $paramAttribute = [Parameter]::new()

    $paramDictionary['Name'] = [System.Management.Automation.RuntimeDefinedParameter]::new('Name', [string], $paramAttribute)
    $paramDictionary['Id'] = [System.Management.Automation.RuntimeDefinedParameter]::new('Id', [int], $paramAttribute)

    return $paramDictionary
}

Function New-Command {
    [CmdletBinding()] # `CmdletBinding` is Mandataroy here
    param()           # if the `param` block is empty

    dynamicparam {
        & $reusedParameters
    }

    end {
        # Caveat: you can reference these parameters via $PSBoundParameters
        #         $Name and $Id are not part of the `param` block
        #         hence that wouldn't work here
        "Name: {0}, ID: {1}" -f $PSBoundParameters['Name'], $PSBoundParameters['ID']
    }
}

New-Command -Name asd -Id 123
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • Can you clarify why it is a bad idea? Given that I know all of these functions are going to need these parameters, having a single point where they are defined so that I only have 1 place to update if I need to modify the validation rules on them seems better for maintainability than copy-pasting the definitions to each function. – David Dec 23 '22 at 10:24
  • 1
    @David Having a `dynamicparam` in your function would make it so it's not possible to create a proxy wrapper around it because `[ProxyCommand]::Create(...)` would not see it. There is also the downside of having to reference these parameters only via `$PSBoundParameters`. The same linked MSFT Doc states that should be only used when needed because they can be difficult for users to discover, however this doesn't apply for this specific example because these 2 parameters would be always available. Not saying you shouldn't do it, if you want to go with it. I personally wouldn't do it. – Santiago Squarzon Dec 23 '22 at 12:52
  • 1
    It is possible to create a proxy wrapper for functions using `dynamicparam`, but it requires a bit of templating: https://stackoverflow.com/a/65628204/7571258. Still, the level of indirection using this approach makes it harder to read the code. – zett42 Dec 23 '22 at 16:42
  • @zett42 well that's unexpected, good to know – Santiago Squarzon Dec 23 '22 at 16:49
1

As a declarative approach, you may turn the common parameters into class properties and have a single function parameter of the class type.

class MyReUsedParameters {
    [String] $Name
    [Int] $Id = 23
}

Function New-Command {
    Param (
        [MyReUsedParameters] $ReUsedParameters,
        $AnotherParam
    )

    Write-Output "Name: $($ReUsedParameters.Name), ID: $($ReUsedParameters.ID)"
}

# Pass the common parameters as a hashtable which gets converted to
# MyReUsedParameters automatically.
New-Command -ReUsedParameters @{ Name = 'foo'; Id = 42 } -AnotherParam bar

# Alternatively pass the common parameters as a (typed) variable.
# PowerShell is able to deduce the argument name from the type.
$commonArgs = [MyReUsedParameters] @{ Name = 'Foo'; Id = 42 }
New-Command $commonArgs -AnotherParam bar

When passing a hashtable or PSCustomObject that has matching properties, it will automatically be converted to the class type.

You may even validate class properties similar to regular parameters. Most parameter validation attributes can be specified for class properties as well.

class MyReUsedParameters {
    [ValidateNotNullOrEmpty()] [String] $Name
    [Int] $Id = 23

    # Constructor - required to apply validation
    MyReUsedParameters( [Hashtable] $ht ) {
        $this.Name = $ht.Name
        $this.Id = $ht.Id
    }
}

Function New-Command {
    Param (
        [Parameter(Mandatory)] 
        [MyReUsedParameters] $ReUsedParameters
    )

    Write-Output "Name: $($ReUsedParameters.Name), ID: $($ReUsedParameters.ID)"
}

# Causes an error (as expected), because Name property is missing
New-Command -ReUsedParameters @{ Id = 42 }
zett42
  • 25,437
  • 3
  • 35
  • 72