| RFC | RFC |
|---|---|
| Author | Darren Kattan, James Brundage |
| Status | Draft |
| Area | Dynamic Parameter Binding |
| Version | 1.0 |
| Comments Due | 2023-12-19 |
| Plan to implement | true |
Facilitate PowerShell's dynamic parameter block by allowing RuntimeDefinedParameter objects to bind arguments incrementally. This will enhance the ability to use previously bound values to refine subsequent parameters dynamically.
As a PowerShell script or module author,
I can use the bound values of one parameter to filter the choices for subsequent parameters,
so that I can offer dynamic and contextual choices to users and enhance the user experience.
Suppose a user wants to write a Get-LatestAvailableMicrosoftEdgeVersion function using data available from https://edgeupdates.microsoft.com/api/products
The data is JSON but is structured as followed
-
Product: Stable
- Platform: Windows
- Architecture: x64
- msi: 118.0.2088.57
- Architecture: arm64
- msi: 118.0.2088.57
- Architecture: x86
- msi: 118.0.2088.57
- Architecture: x64
- Platform: Linux
- Architecture: x64
- rpm: 118.0.2088.57
- deb: 118.0.2088.57
- Architecture: x64
- Platform: MacOS
- Architecture: universal
- pkg: 118.0.2088.57
- plist: 118.0.2088.57
- Architecture: universal
- Platform: Android
- Architecture: arm64
- Version: 118.0.2088.58
- Architecture: arm64
- Platform: iOS
- Architecture: arm64
- Version: 118.0.2088.60
- Architecture: arm64
- Platform: Windows
-
Product: Beta
- Platform: iOS
- Architecture: arm64
- Version: 118.0.2088.36
- Architecture: arm64
- Platform: Linux
- Architecture: x64
- rpm: 119.0.2151.12
- deb: 119.0.2151.12
- Architecture: x64
- Platform: Windows
- Architecture: arm64
- msi: 119.0.2151.12
- Architecture: x64
- msi: 119.0.2151.12
- Architecture: x86
- msi: 119.0.2151.12
- Architecture: arm64
- Platform: MacOS
- Architecture: universal
- plist: 119.0.2151.12
- pkg: 119.0.2151.12
- Architecture: universal
- Platform: Android
- Architecture: arm64
- Version: 119.0.2151.11
- Architecture: arm64
- Platform: iOS
To get the version number the function would require the following parameters:
- Product
- Platform
- Architecture (Not Applicable for Android and iOS)
- PackageType (Applicable for Linux to disambiguate rpm/deb and Windows to disambiguate msi/exe)
With this proposed change, once the user selects a Product and Platform, Architecture and PackageType options are conditionally required based on the API data.
The sample code below demonstrates how this experience might look with the proposed changes:
$EdgeVersions = Invoke-RestMethod https://edgeupdates.microsoft.com/api/products
$productParamAttributes = [Collection[Attribute]]@(
([ValidateSet]::new($EdgeVersions.Product)),
([Parameter]@{ Mandatory = $true })
)
[RuntimeDefinedParameter]::new('Product', [string], $productParamAttributes)
if($Product)
{
$EdgeVersions = $EdgeVersions | ?{$_.Product -eq $Product}
$Platforms = $EdgeVersions | %{ $_.Releases } | select -Unique -Expand Platform
$platformParamAttributes = [Collection[Attribute]]@(
([ValidateSet]::new($Platforms)),
([Parameter]@{ Mandatory = $true })
)
[RuntimeDefinedParameter]::new('Platform', [string], $platformParamAttributes)
if($Platform)
{
$EdgeVersions = $EdgeVersions | ?{$_.Releases.Platform -eq $Platform}
$Architectures = $EdgeVersions.Releases.Architecture | select -Unique | sort
$architectureParamAttributes = [Collection[Attribute]]@(
([ValidateSet]::new($Architectures)),
([Parameter]@{ Mandatory = $true })
)
[RuntimeDefinedParameter]::new('Architecture', [string], $architectureParamAttributes)
if($Architecture)
{
$EdgeVersions = $EdgeVersions | ?{$_.Releases.Architecture -eq $Architecture}
$versionToInstallValues = $EdgeVersions.Releases.ProductVersion | select -Unique
$versionParamAttributes = [Collection[Attribute]]@(
([ValidateSet]::new($versionToInstallValues)),
([Parameter]@{ Mandatory = $true })
)
[RuntimeDefinedParameter]::new('Version', [string], $versionParamAttributes)
}
}
}Upon execution, users will be prompted incrementally:
Product: [List of Products]
Platform: [Filtered List of Platforms]
Architecture: [Filtered List of Architectures]
Version: [Filtered List of Versions]
The syntax could be even more concise with user-supplied helper function New-Parameter
function New-Parameter {
param(
[string]$Name,
[Type]$Type = [object],
[string[]]$ValidValues,
[switch]$Mandatory
)
$attributes = [Collection[Attribute]]@(
([ValidateSet]::new($ValidValues)),
([Parameter]@{ Mandatory = $Mandatory })
)
return [RuntimeDefinedParameter]::new($Name, $Type, $attributes)
}
Function Get-EdgeUpdates
{
param()
dynamicparam
{
$EdgeVersions = Invoke-RestMethod https://edgeupdates.microsoft.com/api/products
New-Parameter -Name Product -ValidValues $EdgeVersions.Product -Type ([string]) -Mandatory
if($Product)
{
$EdgeReleases = $EdgeVersions | ?{$_.Product -eq $Product} | select -Expand Releases # Beta,Stable,Dev,Canary
New-Parameter -Name Platform -ValidValues ($EdgeReleases.Platform | select -Unique) -Type ([string]) -Mandatory # Windows,Linux,MacOS,Android
if($Platform)
{
$EdgeReleases = $EdgeReleases | ?{$_.Platform -eq $Platform}
New-Parameter -Name Architecture -ValidValues ($EdgeReleases.Architecture | select -Unique) -Type ([string]) -Mandatory # x86,x64,arm64,universal
if($Architecture)
{
$EdgeReleases = $EdgeReleases | ?{$_.Architecture -eq $Architecture}
New-Parameter -Name Version -ValidValues ($EdgeReleases.ProductVersion | select -Unique) -Type ([string]) -Mandatory
}
}
}
}
end
{
}
}-
Incremental Binding: As
RuntimeDefinedParameterobjects are written to the output in thedynamicparamblock, they are immediately bound if an argument is available. This behavior differs from the current state, where all parameters are bound after the entiredynamicparamblock has executed. -
Variable Creation: When a
RuntimeDefinedParameteris bound, a variable with the name of that parameter should be created in thedynamicparamblock scope. This makes the bound value immediately available for subsequent logic in thedynamicparamblock. -
Backward Compatibility: This change must not break scripts or modules that use the
dynamicparamblock but do not expect incremental binding. It's crucial to ensure the new behavior doesn't adversely affect existing implementations.
-
Explicit Flag for Incremental Binding: Instead of changing the default behavior, introduce an
IncrementalBindflag toCmdletBindingAttributeor create anIncrementalBindingAttributethat enables incremental binding. This would allow script authors to opt-in to the new behavior explicitly. -
Expose BindParameter() in dynamicparam block: Syntax could look like
dynamicparam
{
$ProductParam = [RuntimeDefinedParameter]::new('Product', [string], $productParamAttributes)
$_.BindParameter($ProductParam)
$Product = $PSBoundParameters['Product']
# or
$Product = $this.BindParameter($ProductParam)
# or
if($_.TryBindParameter($ProductParam, out $Product))
{
...
}
}-
Performance Considerations: Depending on the implementation, there could be performance implications. The exact performance impact should be assessed during the development phase, and optimizations should be considered to ensure that this feature doesn't introduce significant overhead.
-
Discoverability and Documentation Considerations: Perhaps the most reasonable objection to this RFC is discoverability of incrementally bound parameters by Get-Help. For this I propose we either:
- Do nothing as this is already a problem with dynamic parameters that are conditionally present based off the bound value of static parameters
Get-Helpcould appendThis function has dynamic parameters and may require additional parameters not documented here, supply -ArgumentList parameter for additional detailsto commands that implement dynamic/incrementally bound parameters
The syntax could look like
Get-Help MyIncrementallyBindableFunction -Product EdgeThis is similar to
--helpor/hoptions for many command line tools where you can get details about sub-commands.This would be relatively easy to wire up as:
1. `Get-Help` doesn't have any parameters with `ValueFromRemainingArguments` 2. We could pass `-ArgumentList` to `Get-Command` to within `Get-Help` so the incrementally bound parameters become visible
Please note that the above proposal is a starting point and would benefit from feedback, especially concerning discoverability and performance.