There are two distinct questions here:
- How to use
RuntimeDefinedParameterDictionary with IDynamicParameters implementation?
- Why would you choose one approach over the other?
Note: In the following examples I'll be extending the PSCmdlet class (rather than Cmdlet), for ease of access to parameter values bound at runtime.
How to use RuntimeDefinedParameterDictionary
Here's the simplest use case I can think of - unconditionally returning a single [string] parameter $DynamicString (it's not very "dynamic", but it's enough to show the basic difference):
// POCO approach
public class RnosDynamicParameters
{
[Parameter(Mandatory = true)]
public string DynamicString { get; set; }
}
[Cmdlet(VerbsCommon.Get, "RnosGreeting")]
public class GetRnosGreetingCommand : PSCmdlet, IDynamicParameters
{
private RnosDynamicParameters _dynamicParameters;
public object GetDynamicParameters()
{
_dynamicParameters = new RnosDynamicParameters();
return _dynamicParameters;
}
protected override void EndProcessing()
{
WriteObject(string.Format("Hello, {0}!", _dynamicParameters.DynamicString));
}
}
// Runtime-defined dictionary approach
[Cmdlet(VerbsCommon.Get, "RnosGreeting2")]
public class GetRnosGreetingCommand2 : PSCmdlet, IDynamicParameters
{
public object GetDynamicParameters()
{
// Create dictionary
var dictionary = new RuntimeDefinedParameterDictionary();
// Create parameter definition
var parameter = new RuntimeDefinedParameter();
parameter.Name = "DynamicString";
parameter.ParameterType = typeof(string);
// Add parameter attribute
var paramAttribute = new ParameterAttribute();
paramAttribute.Mandatory = true;
parameter.Attributes.Add(paramAttribute);
// Add parameter definition to dictionary and return
dictionary.Add(parameter.Name, parameter);
return dictionary;
}
protected override void EndProcessing()
{
WriteObject(string.Format("Hello, {0}!", this.MyInvocation.BoundParameters["DynamicString"]));
}
}
Both will result in the exact same behavior:
PS ~> Get-RnosGreeting -DynamicString "Mathias"
Hello, Mathias!
PS ~> Get-RnosGreeting2 -DynamicString "Mathias"
Hello, Mathias!
PS ~> Get-RnosGreeting -DynamicString
Get-RnosGreeting : Missing an argument for parameter 'DynamicString'. Specify a parameter of type 'System.String' and try again.
At line:1 char:18
+ Get-RnosGreeting -DynamicString
+ ~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Get-RnosGreeting], ParameterBindingException
+ FullyQualifiedErrorId : MissingArgument,RnosDynamicParams.GetRnosGreetingCommand
PS ~> Get-RnosGreeting2 -DynamicString
Get-RnosGreeting2 : Missing an argument for parameter 'DynamicString'. Specify a parameter of type 'System.String' and try again.
At line:1 char:19
+ Get-RnosGreeting2 -DynamicString
+ ~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Get-RnosGreeting2], ParameterBindingException
+ FullyQualifiedErrorId : MissingArgument,RnosDynamicParams.GetRnosGreetingCommand2
So, while using a RuntimeDefinedParameterDictionary looks a little more bloated at this point, you can see it allows us to programmatically determine and modify the (otherwise attribute-based) behavior of the dynamic parameter(s) at runtime - instead of decorating the parameter object (your POCO) ahead of time.
Why would you choose one approach over the other?
As you can see, using a RuntimeDefinedParameterDictionary is not quite as clean and maintainable as a POCO - so why would you choose the dictionary approach over the other?
The answer is flexibility - deferring construction of the parameter definition until the very last minute means we can make very granular choices about the behavior of individual dynamic parameters based on partial user input, with minimal additional code.
Let's do a slightly more advanced example - we'll decide whether to mark the DynamicString parameter Mandatory or not, based on whether the user has already specified a certain input value for another parameter, to get a behavior like:
PS ~> Get-RnosGreeting
Hello, World!
PS ~> Get-RnosGreeting -CustomGreetingRequired -DynamicString
Get-RnosGreeting : Missing an argument for parameter 'DynamicString'. Specify a parameter of type 'System.String' and try again.
At line:1 char:42
+ Get-RnosGreeting -CustomGreetingRequired -DynamicString
+ ~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Get-RnosGreeting], ParameterBindingException
+ FullyQualifiedErrorId : MissingArgument,RnosDynamicParams.GetRnosGreetingCommand
But now we run into a problem - attribute decorations (like [Parameter(Mandatory = true)]) are tied to the identity of the enclosing type at compile-time, and there's no straightforward way for us to "override" this metadata at runtime:
[Cmdlet(VerbsCommon.Get, "RnosGreeting")]
public class GetRnosGreetingCommand : PSCmdlet, IDynamicParameters
{
[Parameter(Mandatory = false)]
public SwitchParameter CustomGreetingRequired { get; set; }
private IRnosDynamicParameters _dynamicParameters;
interface IRnosDynamicParameters
{
string DynamicString { get; set; }
}
public class RnosDynamicParametersMandatory : IRnosDynamicParameters
{
[Parameter(Mandatory = true)]
public string DynamicString { get; set; }
}
public class RnosDynamicParametersOptional : IRnosDynamicParameters
{
[Parameter(Mandatory = false)]
public string DynamicString { get; set; }
}
public object GetDynamicParameters()
{
if (CustomGreetingRequired.IsPresent)
{
_dynamicParameters = new RnosDynamicParametersMandatory();
}
else
{
_dynamicParameters = new RnosDynamicParametersOptional();
}
return _dynamicParameters;
}
protected override void EndProcessing()
{
var greetingTarget = "World";
if (MyInvocation.BoundParameters.ContainsKey(nameof(_dynamicParameters.DynamicString)))
{
greetingTarget = _dynamicParameters.DynamicString;
}
WriteObject(string.Format("Hello, {0}!", greetingTarget));
}
}
... so we end up having to write lots of code to keep it somewhat clean.
With the dictionary approach on the other hand, it's a single statement change:
// Runtime-defined dictionary approach
public class GetRnosGreetingCommand2 : PSCmdlet, IDynamicParameters
{
[Parameter(Mandatory = false)]
public SwitchParameter CustomGreetingRequired { get; set; }
public object GetDynamicParameters()
{
// Create dictionary
var dictionary = new RuntimeDefinedParameterDictionary();
// Create parameter definition
var parameter = new RuntimeDefinedParameter();
parameter.Name = "DynamicString";
parameter.ParameterType = typeof(string);
// Add parameter attribute
var paramAttribute = new ParameterAttribute();
paramAttribute.Mandatory = CustomGreetingRequired.IsPresent;
parameter.Attributes.Add(paramAttribute);
// Add parameter definition to dictionary and return
dictionary.Add(parameter.Name, parameter);
return dictionary;
}
protected override void EndProcessing()
{
var greetingTarget = "World";
if (MyInvocation.BoundParameters.ContainsKey("DynamicString"))
{
greetingTarget = (string)MyInvocation.BoundParameters["DynamicString"];
}
WriteObject(string.Format("Hello, {0}!", greetingTarget));
}
}
Notice how the only material change we made was from:
paramAttribute.Mandatory = true;
to
paramAttribute.Mandatory = CustomGreetingRequired.IsPresent;
Everything else (the changes in EndProcessing()) was required in either case, but with the dictionary approach we can make last-minute decisions with minimal code.
So, to sum up:
POCO/decorator approach:
- Pros:
- Clean (eg.
nameof(_dynamicParams.DynamicString) over "DynamicString")
- Cons:
- Must explicitly define many parameter types to support truly "dynamic" runtime behavior
Runtime-defined dictionary approach:
- Pros:
- Extremely flexible (change parameter behavior easily at runtime)
- Cons:
- Compiler/type system won't hold your hand to the same extent
As a rule of thumb, if the complexity of your dynamic parameter "decision tree" exceeds a simple "yes/no" to exposing a set of parameters that always behave the same, you should probably consider the dictionary.