当参数来自管道时,为什么这个 Powershell Cmdlet return 是一个多维数组?

Why does this Powershell Cmdlet return a multidimensional array when parameter comes from pipeline?

我用 C# 编写了一个 Powershell cmdlet,其中 return 使用自己开发的 API 详细介绍了一名或多名员工的直接经理。该 cmdlet 应该 return 一个或多个类型为 Associate 的对象的集合。我遇到的问题是 Cmdlet 的输出类型不一致。

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

这是我所看到的行为,但就 Cmdlet 输出而言:

    > $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

这是 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 绑定到身份
  • 调用ProcessRecord()
  • 调用EndProcessing()

当您将等效数组传递给它时(例如 "A","B" |Get-Manager),PowerShell 会枚举输入并将项目绑定到适当的参数 一个接一个相反 - 即 PowerShell:

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

...结果是 2 个 List<Associate>,而不是一个。

“解决方案”是:

  1. 不是 return 作为输出对象的具体集合类型,或
  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。累计输出,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);
}

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