1

I am trying to write a simple PowerShell cmdlet in C# that accepts an instance of ArrayList and adds an item to it. The issue I've encountered is that a copy of the array list is passed to cmdlet, not the actual object as I would expect. Is there a way to pass parameter by reference? The following code outputs just "Count is 0" for ArrayList $B.

Note: to run the program in PowerShell 7.1 you need to target .NET 5 and install Microsoft.PowerShell.SDK and System.Management.Automation Nuget packages.


using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Runspaces;

namespace CustomPowerShellCmdlet
{
    class Program
    {
        static void Main(string[] args)
        {
            string powerShellScript = @"

$B  = New-Object System.Collections.ArrayList
Add-CollectionItem -Collection $B -Item '1'
Add-CollectionItem -Collection $B -Item '2'

Write-Output  ('Count is ' + $B.Count)
            ";


            var output = RunPowerShellScript(powerShellScript);

            foreach (PSObject psObject in output)
            {
                Console.WriteLine(psObject.BaseObject.ToString());
            }
        }

        private static Collection<PSObject> RunPowerShellScript(string powerShellScript)
        {
            InitialSessionState initialSessionState = InitialSessionState.CreateDefault();
            initialSessionState.Commands.Add(new SessionStateCmdletEntry("Add-CollectionItem", typeof(AddCollectionItemCmdlet), null));

            using (var runspace = RunspaceFactory.CreateRunspace(initialSessionState))
            {
                runspace.Open();

                using (var ps = PowerShell.Create())
                {
                    ps.Streams.Error.DataAdding += Output_DataAdding;
                    ps.Streams.Debug.DataAdding += Output_DataAdding;
                    ps.Runspace = runspace;

                    ps.AddScript(powerShellScript);

                    return ps.Invoke();
                }
            }
        }

        private static void Output_DataAdding(object sender, DataAddingEventArgs e)
        {
            Console.WriteLine(e.ItemAdded.ToString());
        }

        [Cmdlet(VerbsCommon.Add, "CollectionItem")]
        public class AddCollectionItemCmdlet : Cmdlet
        {
            [Parameter(Mandatory = true)]
            [AllowEmptyCollection]
            public ArrayList Collection { get; set; }

            [Parameter(Mandatory = true)]
            public object Item { get; set; }

            /// <summary>
            /// This static field is needed for demo purposes only.
            /// </summary>
            public static List<object> AddedItems = new List<object>();

            protected override void BeginProcessing()
            {
                Console.WriteLine("A new collection is passed to the cmdlet: {0}", !AddedItems.Any(x => ReferenceEquals(x, Collection)));
                AddedItems.Add(Collection);

                Collection.Add(Item);
            }
        }
    }
}

UPD1. It appears this behavior is caused by New-Object cmdlet. If I replace it with [System.Collections.ArrayList] @() then I get what I need. Still I don't understand why it works this way.

2
  • What version of PowerShell? I'm unable to reproduce this behavior both in PowerShell 5.1, 7.0.3 and 7.1.0. Perhaps you're testing in a session with a previous version of your module loaded? Commented Dec 10, 2020 at 16:32
  • @MathiasR.Jessen I can reproduce it in 5.1 and 7.1. I updated the code in the question to clarify how the cmdlet is used. It is registered as a command in the Runspace, I am not using it in a module. Commented Dec 10, 2020 at 20:08

2 Answers 2

0

The object is being passed to the cmdlet, but it is not being returned, i.e. set is called, but get is not. You'll need to return the collection via the pipeline and reassign it to the original variable. Here is the working code:

using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Management.Automation;
using System.Management.Automation.Runspaces;

namespace CustomPowerShellCmdlet
{
    class Program
    {
        static void Main(string[] args)
        {
            string powerShellScript = @"
Write-Output ('PowerShell Version is ' + $PSVersionTable.PSVersion)

$A  = New-Object System.Collections.ArrayList
$A.Add('3') | Out-Null
Write-Output  ('Count is ' + $A.Count)

$B  = New-Object System.Collections.ArrayList
$B = (Add-CollectionItem -Collection $B -Item '1')
$B = (Add-CollectionItem -Collection $B -Item '2')
foreach ($Item in $B) {
    Write-Output $Item
}
Write-Output  ('Count is ' + $B.Count)
";
            var output = RunPowerShellScript(powerShellScript);

            foreach (PSObject psObject in output)
            {
                Console.WriteLine(psObject.BaseObject.ToString());
            }
        }

        private static Collection<PSObject> RunPowerShellScript(string powerShellScript)
        {
            InitialSessionState initialSessionState = InitialSessionState.CreateDefault();
            initialSessionState.Commands.Add(new SessionStateCmdletEntry("Add-CollectionItem", typeof(AddCollectionItemCmdlet), null));

            using (var runspace = RunspaceFactory.CreateRunspace(initialSessionState))
            {
                runspace.Open();
                using (var ps = PowerShell.Create())
                {
                    ps.Runspace = runspace;
                    ps.AddScript(powerShellScript);
                    return ps.Invoke();
                }
            }
        }

        [Cmdlet(VerbsCommon.Add, "CollectionItem")]
        public class AddCollectionItemCmdlet : Cmdlet
        {
            [Parameter(Mandatory = true)]
            [AllowEmptyCollection]
            public ArrayList Collection
            {
                get; set;
            }

            [Parameter(Mandatory = true)]
            public object Item { get; set; }

            protected override void ProcessRecord()
            {
                Collection.Add(Item);
                WriteObject(Collection);
            }
        }
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

I tried that as well, visually it seems to do what I need, but behind the scenes it is still a copy of the collection. I slightly modified my code to include debug info. Regardless of the approach, it outputs ' A new collection is passed to the cmdlet: True A new collection is passed to the cmdlet: True'
0

It appeared this is a bug in PowerShell, see https://github.com/PowerShell/PowerShell/issues/14394

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.