Reputation: 634
I've written a Powershell cmdlet in C# which returns details about one or more employees' direct manager(s) using a home-grown API. The cmdlet is supposed to return a collection of one or more objects of type Associate. The problem I'm having is that the output type of the Cmdlet is not consistent.
I designed the Cmdlet such that if you already have a collection of Associate objects, you can pass that in via the pipeline. Otherwise, you need to pass in one or more userIds under the -Identity parameter.
Here is the behavior I'm seeing, though in terms of the Cmdlet output:
> $test1 = Get-Manager -Identity 'user1','user2'
> $test1.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
PS H:\> $test1 | select displayName
displayName
-----------
John Doe
Jane Lee
> $folks = Get-Associate 'brunomik','abcdef2'
> $test2 = Get-Manager -Assoc $folks
> $test2.getType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
PS H:\> $test2 | Select displayName
displayName
-----------
John Doe
Jane Lee
> $test3 = $folks | Get-Manager
> $test3.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
> $test3 | select displayName
displayName
-----------
># Select-Object can't find a property called displayName
># But if I run GetType() on the first element of the collection:
> $test3[0].GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
># It appears to be yet another collection!
># Now, if I run Select-Object on that first element of $test3, I do see the data:
> $test3[0] | Select displayName
displayName
-----------
John Doe
Jane Lee
Here is the source code for the Cmdlet:
[Cmdlet(VerbsCommon.Get, "Manager", DefaultParameterSetName = @"DefaultParamSet")]
[OutputType(typeof(Associate))]
public class GetManager : Cmdlet
{
private Associate[] assoc = null;
private string[] identity = null;
private bool assocSet = false;
private bool identitySet = false;
//The Assoc parameter supports the pipeline and accepts one or more objects of type Associate
[Parameter(ParameterSetName = @"DefaultParamSet",
ValueFromPipeline = true,
HelpMessage = "An Associate object as returned by the \"Get-Associate\" cmdlet. Cannot be used with the \"Identity\" parameter")]
public Associate[] Assoc
{
get
{
return assoc;
}
set
{
assoc = value;
assocSet = true;
}
}
//The Identity parameter accepts one or more string expressions (user IDs)
[Parameter(HelpMessage = "An Associate user Id. Not to be used with the \"Assoc\" parameter")]
public string[] Identity
{
get
{
return identity;
}
set
{
identitySet = true;
identity = value;
}
}
//This will contain the output of the Cmdlet
private List<Associate> Result = new List<Associate>();
protected override void BeginProcessing()
{
base.BeginProcessing();
}
protected override void ProcessRecord()
{
base.ProcessRecord();
BuildOutputObject();
WriteObject(Result);
}
//Builds the Cmdlet Output object
private void BuildOutputObject()
{
List<Associate> Subordinates = new List<Associate>();
//Only the Assoc or Identity parameter may be set; not both.
if (!(assocSet ^ identitySet))
{
throw new ApplicationException($"Either the {nameof(Assoc).InQuotes()} or the {nameof(Identity).InQuotes()} parameter must be set, but not both.");
}
//If Assoc is set, we already have an array of Associate objects, so we'll simply define Subordinates by calling Assoc.ToList()
if (assocSet)
{
Subordinates = Assoc.ToList();
}
//Otherwise, we'll need to create an associate object from each userID passed in with the "Identity" parameter. The MyApi.GetAssociates() method returns a list of Associate objects.
else
{
Subordinates = MyApi.GetAssociates(Identity);
if (!MyApi.ValidResponse)
{
throw new ApplicationException($"No associate under the identifiers {string.Join(",",Identity).InQuotes()} could be found.");
}
}
//Now, to build the output object:
Subordinates.ForEach(p => Result.Add(p.GetManager()));
}
}
Upvotes: 1
Views: 177
Reputation: 174485
ProcessRecord
is executed once per input argument.
As a result, when you call Get-Manager -Identity A,B
, PowerShell:
BeginProcessing()
A,B
to IdentityProcessRecord()
EndProcessing()
When you pipe an equivalent array to it (eg. "A","B" |Get-Manager
), PowerShell enumerates the input and binds the items to the appropriate parameter one-by-one instead - that is, PowerShell:
BeginProcessing()
A
to Identity
ProcessRecord()
B
to Identity
ProcessRecord()
EndProcessing()
... resulting in 2 List<Associate>
's, instead of one.
The "solution" is to either:
ProcessRecord
, then output once, in EndProcessing
.IEnumerable
typesThis approach closely resembles an iterator method in C# - think of WriteObject(obj);
as PowerShell's version of yield return obj;
:
protected override void ProcessRecord()
{
base.ProcessRecord();
BuildOutputObject();
foreach(var obj in Result)
WriteObject(obj);
}
WriteObject()
also has an overload that enumerates the object for you, so the simplest fix is actually just:
protected override void ProcessRecord()
{
base.ProcessRecord();
BuildOutputObject();
WriteObject(Result, true);
}
This first option is by far the most preferable, as it allows us to take optimal advantage of performance characteristics of PowerShell's pipeline processor.
WriteObject()
in EndProcessing()
:private List<Associate> finalResult = new List<Associate>();
protected override void ProcessRecord()
{
base.ProcessRecord();
BuildOutputObject();
# Accumulate output
finalResult.AddRange(Result)
}
protected override void EndProcessing()
{
WriteObject(finalResult);
}
Omitting the second argument from WriteObject
and only calling it once, will preserve the type of finalResult
, but you will be blocking any downstream cmdlets from executing until this one is done processing all input
Upvotes: 1