当参数来自管道时,为什么这个Powershell Cmdlet返回多维数组



我在C#中编写了一个Powershell cmdlet,该cmdlet使用home-grown API返回一个或多个员工的直接经理的详细信息。cmdlet应该返回一个或多个类型为Associate的对象的集合。我遇到的问题是Cmdlet的输出类型不一致。

我设计Cmdlet的方式是,如果您已经有Associate对象的集合,则可以通过管道将其传入。否则,您需要在-Identity参数下传入一个或多个userId。

以下是我看到的行为,尽管是就Cmdlet输出而言:

  • 如果我传入一个或多个带有-Identity参数的userId,我将获得Associate的预期集合:
> $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
  • 如果我通过显式使用-Assoc参数传入一个或多个Associate对象,我也会得到预期的集合
> $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
  • 但是,如果我使用管道传入Associate对象的集合,我似乎会得到一个多维数组!:
> $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

以下是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()));
}
}

ProcessRecord每个输入参数执行一次。

因此,当您调用Get-Manager -Identity A,B时,PowerShell:

  • 解析适当的参数集(如有必要(
  • 调用BeginProcessing()
  • 将值A,B绑定到Identity
  • 调用ProcessRecord()
  • 调用EndProcessing()

当您通过管道将等效数组(例如"A","B" |Get-Manager(发送到它时,PowerShell会枚举输入并将项逐一绑定到相应的参数,即PowerShell:

  • 解析适当的参数集(如有必要(
  • 调用BeginProcessing()
  • 将值A绑定到Identity
  • 调用ProcessRecord()
  • 将值B绑定到Identity
  • 调用ProcessRecord()
  • 调用EndProcessing()

。。。导致2个List<Associate>而不是1个。

";解决方案";是指:

  1. not将具体集合类型作为输出对象返回,或者
  2. "收集";在ProcessRecord中部分输出,然后在EndProcessing中输出一次

1.IEnumerable类型中没有包装

这种方法非常类似于C#中的迭代器方法-将WriteObject(obj);视为PowerShell版本的yield return obj;:

protected override void ProcessRecord()
{
base.ProcessRecord();
BuildOutputObject();
foreach(var obj in Result)
WriteObject(obj);
}

WriteObject()还有一个重载,可以为您枚举对象,所以最简单的修复方法实际上只是:

protected override void ProcessRecord()
{
base.ProcessRecord();
BuildOutputObject();
WriteObject(Result, true);
}

到目前为止,第一个选项是,因为它使我们能够最佳利用PowerShell管道处理器的性能特征。

2.累加输出,EndProcessing()中的WriteObject()

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);
}

省略WriteObject中的第二个参数,只调用一次,将保留finalResult的类型,但您将阻止任何下游cmdlet执行,直到该cmdlet处理完所有输入

最新更新