简体   繁体   中英

Design Patterns for PowerShell Cmdlets (in C#)

Working on a PowerShell Module in C# and something occurred to that seems logical but I can't find any other info to confirm or correct my assumptions.

Say I have one Cmdlet looks something like this

[Cmdlet(VerbsCommon.Get, CmdletHelpers.Noun, SupportsShouldProcess = true)]
[CmdletBinding]
public class GetSomething : PSCmdlet
{
    protected override void ProcessRecord() 
    { 
        var SomeCustomObject = new SomeCustomClass()
        WriteObject(SomeCustomObject); 
    }
}

Then say I have another Cmdlet which accepts a SomeCustomObject as a Parameter.

[Cmdlet(VerbsData.Import, CmdletHelpers.Noun)]
[CmdletBinding]
public class ImportSomething : PSCmdlet
{

    [Parameter(Mandatory = true, ValueFromPipeline = true)]
    public SomeCustomClass SomeCustomObject { get; set; }

    protected override void ProcessRecord()
    {
        // Do Something with SomeCustomObject
        // Return modified SomeCustomObject
        WriteObject(SomeCustomObject);
    }
}

So I would assume that a PowerShell Cmdlet that takes in a particular parameter should usually not modify the parameter and instead create a copy if changes need to be made to that parameter. However I can't find any definitive opinions on this.

In other words if I change my ProcessRecord above to

protected override void ProcessRecord()
{
    SomeCustomObject.ChangeSomething();
    WriteObject(SomeCustomObject);
}

If I have some PowerShell calls:

$SCO = Get-Something
$NewSCO = $SCO | Import-Something

$NewSCO AND $SCO BOTH would have been modified to the same state which seems bad, ie we'd always want the items upstream in the pipeline to retain their state so to speak so if later on we want to go back it always retains its original values when it was previously down stream of the Cmdlet that created it.

So instead (Assuming SomeCustomClass has a Copy Constructor) this should be the better design pattern.

protected override void ProcessRecord()
{
    var NewSomeCustomObject = new SomeCustomClass(SomeCustomObject);
    NewSomeCustomObject.ChangeSomething();
    WriteObject(NewSomeCustomObject);
} 

Am I completely off here?

Would there ever be a GOOD reason ESPECIALLY in the pipeline to modify a parameter object upstream of the current Cmdlet?

TofuBug, I completely agree with your conclusion for the case you have indicated. That is, one should consider cmdlet parameters as inviolate for the very reason you outlined, that any reasonable person reading the code would never suspect that your $SCO was modified after being passed along in the pipeline!

But let's consider this a bit further. I would posit that the reason you have found no supporting discussion on this is that the vast majority of users are already treating parameters as read-only inputs without giving it any second thought, because it is the path of least resistance in PowerShell. That is, most parameters tend to be simple objects (strings, numbers, and Booleans)--even when you pass in a composite object.

Let's get a bit more concrete with your example to see what I mean. Say SomeCustomClass has two properties:

public class SomeCustomClass
{
    public string Name { get; set; }
    public string Description { get; set; }
}

Then your original classes might be:

[Cmdlet(VerbsCommon.Get, "Something")]
public class GetSomething : PSCmdlet
{
    protected override void ProcessRecord()
    {
        var SomeCustomObject = new SomeCustomClass
        { Name = "some name", Description = "some description" };
        WriteObject(SomeCustomObject);
    }
}

[Cmdlet(VerbsData.Import, "Something")]
public class ImportSomething : PSCmdlet
{
    [Parameter(Mandatory = true, ValueFromPipeline = true)]
    public SomeCustomClass SomeCustomObject { get; set; }

    protected override void ProcessRecord()
    {
        SomeCustomObject.Name = "new name"; // modify the input object--bad!
        WriteObject(SomeCustomObject);
    }
}

But if instead of using the entire object as a parameter, grab the specific properties of interest. In the revised cmdlet below, there are now two parameters matched to the properties of the original input object with ValueFromPipelineByPropertyName . Because there is no original SomeCustomClass object to modify, you are forced to create a new one ( outputVar ). Note that in ProcessRecord , I am modifying an input parameter ( Name ) just like you did with SomeCustomObject , but here it is safe to do because it is not a "pointer" to a pipelined input, just a copy of one the input's properties.

[Cmdlet(VerbsData.Import, "SomethingElse")]
public class ImportSomethingElse : PSCmdlet
{
    [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true)]
    public string Name { get; set; }

    [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true)]
    public string Description { get; set; }

    protected override void ProcessRecord()
    {
        Name = "new name";
        var outputVar = new SomeCustomClass { Name = Name, Description = Description}
        WriteObject(outputVar);
    }
}

If you run your example with the new Import-SomethingElse it will show what we want: the original object has not been modified.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM