Boris Nikitin
Boris Nikitin

Reputation: 429

Can't pipe objects from .Net to hosted powershell

I want to execute Powershell script in C# application. For this I'm using Microsoft.PowerShell.5.ReferenceAssemblies package (because of net48 requirements).

I'd like to pipe objects from .Net to script but have no success.

        static void Main(string[] args)
        {
            var runspace = RunspaceFactory.CreateRunspace();
            runspace.Open();

            var ps = PowerShell.Create();
            ps.Runspace = runspace;

            var pipeline = runspace.CreatePipeline();
            pipeline.Commands.AddScript(@"example.ps1");

            pipeline.Input.Write("1");
            pipeline.Input.Write("2");
            pipeline.Input.Flush();
            
            var res = pipeline.Invoke();
            foreach (var xx in res) {
                Console.WriteLine(xx);
            }
        }

Example.ps1:

[CmdletBinding()]
param (
    [Parameter(ValueFromPipeline=$true)]
    $p
)

Begin {
    Write-Output "Start"
}
    
Process {
    Write-Output "Process $_"
}
    
End {
    Write-Output "End"
}

Actual output:

Start
Process
End

Expected output:

Start
Process 1
Process 2
End

Upvotes: 1

Views: 375

Answers (1)

mclayton
mclayton

Reputation: 10083

After a load of empirical testing, I've found the following works for me:

pipeline.Input.Write("1");
pipeline.Input.Write("2");
pipeline.Commands.Add(".\\example.ps1");

That is, use pipeline.Commands.Add, not pipeline.Commands.AddScript. Even though your command is the name of a script file, it's a command not a script!

Also, note the pipeline.Input.Write comes before the pipeline.Commands.Add, and I've removed the pipeline.Input.Flush() - these all seemed to break things in different ways.

The full code becomes:

static void Main(string[] args)
{

    var runspace = RunspaceFactory.CreateRunspace();
    runspace.Open();

    var ps = PowerShell.Create();
    ps.Runspace = runspace;

    var pipeline = runspace.CreatePipeline();

    pipeline.Input.Write("1");
    pipeline.Input.Write("2");
    pipeline.Commands.Add(".\\example.ps1");

    var res = pipeline.Invoke();
    foreach (var xx in res) {
        Console.WriteLine(xx);
    }

}

and the application writes this out to the console:

Start
Process 1
Process 2
End

The corollary is, it looks like AddScript doesn't consume pipeline input.

For completeness, here's some of the other (broken) tests I did along the way:

// fails - original code
//pipeline.Commands.AddScript(".\\example.ps1");
//pipeline.Input.Write("1");
//pipeline.Input.Write("2");
//pipeline.Input.Flush();
// Start
// Process
// End

// test - works for a command, but requires in-script parameters
//pipeline.Commands.AddScript("1, 2 | write-output");
// 1
// 2

// test - works for a script, but requires in-script parameters
//pipeline.Commands.AddScript("1, 2 | .\\example.ps1");
// Start
// Process 1
// Process 2
// End

// test - works for a command with pipeline input
//pipeline.Input.Write("1");
//pipeline.Input.Write("2");
//pipeline.Commands.Add("write-output");
// 1
// 2

// test - fails with AddScript and mandatory parameters on script
//pipeline.Input.Write("1");
//pipeline.Input.Write("2");
//pipeline.Commands.AddScript("write-output");
// System.Management.Automation.ParameterBindingException: 'Cannot process command because of one
// or more missing mandatory parameters: InputObject.'

// test - fails with AddScript and optional parameters on script
//pipeline.Input.Write("1");
//pipeline.Input.Write("2");
//pipeline.Commands.AddScript(".\\example.ps1");
// Start
// Process
// End

// fails - flush kills pipelne
//pipeline.Input.Write("1");
//pipeline.Input.Write("2");
//pipeline.Input.Flush();
//pipeline.Commands.Add(".\\example.ps1");
// Start
// Process
// End

// fails - pipeline written after script
//pipeline.Commands.AddScript(".\\example.ps1");
//pipeline.Input.Write("1");
//pipeline.Input.Write("2");
// Start
// Process
// End

Update

You can turn a string containing a PowerShell script into a "pipeline-able" command as follows (note the second parameter in new Command - i.e. isScript: true):

pipeline.Input.Write("1");
pipeline.Input.Write("2");

var scriptText = File.ReadAllText(".\\example.ps1");
pipeline.Commands.Add(
    new Command(scriptText, true)
);

// Start
// Process 1
// Process 2
// End

Upvotes: 1

Related Questions