我在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个。
";解决方案";是指:
- not将具体集合类型作为输出对象返回,或者
- "收集";在
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处理完所有输入