Core/Get-TestProviderParameter.ps1

function Get-TestProviderParameter {
    <#
    .SYNOPSIS
        Builds a dynamic parameter set mirroring a provider command's own parameters

    .DESCRIPTION
        The shared dispatch functions - New-TestEnvironment, Remove-TestEnvironment and the
        rest - forward to whichever provider the session is connected through. They need to
        accept whatever that provider accepts, and the alternatives are all worse:

        - Restating each provider's parameters by hand means every provider change needs a
          matching edit here, and the one that gets forgotten fails by silently not forwarding.
        - ValueFromRemainingArguments accepts anything and binds nothing, so a typo becomes a
          runtime surprise instead of a binding error, and tab completion stops working.

        So the target command's real parameters are reflected over and re-declared. That gives
        tab completion, Get-Help, mandatory enforcement and type checking on the dispatcher,
        all sourced from the provider rather than duplicated from it.

        Common parameters are excluded, because PowerShell adds those itself and re-declaring
        one is an error rather than a no-op.

    .PARAMETER CommandName
        The provider command whose parameters should be mirrored. A name that does not resolve
        returns an empty dictionary rather than throwing, because dynamicparam runs during
        binding - before the dispatcher has had a chance to report that nothing is connected.

    .PARAMETER Exclude
        Parameters the caller declares itself. Re-declaring one is not an override but an
        error - "a parameter with the name 'PassThru' was defined multiple times" - so a
        dispatcher that keeps a parameter of its own has to name it here.

    .OUTPUTS
        System.Management.Automation.RuntimeDefinedParameterDictionary

    .EXAMPLE
        PS> Get-TestProviderParameter -CommandName 'New-EntraEnvironment'

        DESCRIPTION: Mirrors the Entra provider's parameters onto the caller
        OUTPUT: A dictionary the dynamicparam block returns
        USE CASE: Called from every shared dispatch function

    .NOTES
        Author: Jeffrey Stuhr
        Blog: https://www.techbyjeff.net
        LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/
    #>


    [CmdletBinding()]
    [OutputType([System.Management.Automation.RuntimeDefinedParameterDictionary])]
    param(
        [Parameter()]
        [AllowNull()]
        [AllowEmptyString()]
        [string]$CommandName,

        [Parameter()]
        [string[]]$Exclude = @()
    )

    $dictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()

    if ([string]::IsNullOrWhiteSpace($CommandName)) { return $dictionary }

    $command = Get-Command -Name $CommandName -ErrorAction SilentlyContinue
    if (-not $command) { return $dictionary }

    # PowerShell supplies these itself; re-declaring one throws rather than being ignored.
    $common = [System.Management.Automation.PSCmdlet]::CommonParameters +
        [System.Management.Automation.PSCmdlet]::OptionalCommonParameters

    foreach ($entry in $command.Parameters.GetEnumerator()) {
        if ($common -contains $entry.Key) { continue }
        if ($Exclude -contains $entry.Key) { continue }

        $attributes = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()

        # Parameter attributes are copied rather than recreated, so mandatory-ness, parameter
        # sets and pipeline binding all survive. Validation attributes come with them, which is
        # what keeps a bad value failing at the dispatcher rather than inside the provider.
        foreach ($attribute in $entry.Value.Attributes) {
            $attributes.Add($attribute)
        }

        $dictionary.Add($entry.Key,
            [System.Management.Automation.RuntimeDefinedParameter]::new(
                $entry.Key, $entry.Value.ParameterType, $attributes))
    }

    return $dictionary
}