In this article, we’ll learn how to execute a PowerShell script in C# using the ProcessStartInfo class from System.Diagnostics namespace and PowerShell classes from the System.Management.Automation namespace, which is available in PowerShell.SDK.

To download the source code for this article, you can visit our GitHub repository.

Let’s see how to do that!

Use the ProcessStartInfo Class to Execute a PowerShell Script in C#

.NET provides users with a ProcessStartInfo class that enables us to configure, start and stop a process. We can leverage this class to invoke PowerShell commands and scripts. To learn more about ProcessStartInfo class, please check the article on How to execute CLI Applications From C#.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Let’s start by creating a simple .ps1 file with this content:

I am invoked using ProcessStartInfoClass!

Next, we will create a function to execute our script:

public string ExecuteScript(string pathToScript)
{
    var scriptArguments = "-ExecutionPolicy Bypass -File \"" + pathToScript + "\"";
    var processStartInfo = new ProcessStartInfo("powershell.exe", scriptArguments);
    processStartInfo.RedirectStandardOutput = true;
    processStartInfo.RedirectStandardError = true;
    
    using var process = new Process();
    process.StartInfo = processStartInfo;
    process.Start();
    string output = process.StandardOutput.ReadToEnd();
    string error = process.StandardError.ReadToEnd();
    Console.Writeline(output); // I am invoked using ProcessStartInfoClass!
}

When we create a new ProcessStartInfo instance we have to provide two arguments – the file name and command line arguments. Thus, we set the ExecutionPolicy Bypass, which ensures that we can run our scripts with no restrictions.

Then, we invoke the process using ProcessStartInfo class. We do that by creating a new Process instances and assigning the created processStartInfo object to the StartInfo property.

When we start a process using Process.Start, the process has its own standard output and standard error streams where it will write output and error messages. By default, the calling process is not catching these streams. That is why we set RedirectStandardOutput and RedirectStandardError properties to  true. We instruct the Process object to redirect the standard output and standard error streams so that we can capture and read them programmatically.

Lastly, we take the results from the output and error streams respectively and safely dispose of the process by including using statement.

Now, let’s modify our code and see how simple we can execute PowerShell commands directly instead of running a script. All we need to do is change the setup of the ProcessStartInfo class slightly:

public string ExecuteCommand(string command)
{
    var processStartInfo = new ProcessStartInfo();
    processStartInfo.FileName = "powershell.exe";
    processStartInfo.Arguments = $"-Command \"{command}\"";
    processStartInfo.UseShellExecute = false;
    processStartInfo.RedirectStandardOutput = true;
    
    using var process = new Process();
    process.StartInfo = processStartInfo;
    process.Start();
    string output = process.StandardOutput.ReadToEnd();
    Console.Writeline(output);
}

We configure it in a similar manner except now the Arguments include a command variable that we will specify when invoking the function:

ExecuteCommand("I am invoked using echo command!"); // I am invoked using echo command!

To sum up, when we use the ProcessStartInfo class to execute a PowerShell script, we actually start a PowerShell.exe on our local system by calling process.Start and passing it all the necessary arguments to execute our commands.

Use the PowerShell Class to Execute a PowerShell Script in C#

We can also use the PowerShell class from the PowerShell.SDK to execute our commands and scripts in C#. Let’s start by referencing the  Microsoft.PowerShell.SDK, which gives us access to the System.Management.Automation namespace:

using System.Management.Automation;

PowerShell.SDK offers us a lot of flexibility for executing commands and scripts and enables us to create custom runspaces, which we will cover later.

The main difference between using the PowerShell class and the ProcessStartInfo class is that the PowerShell class creates our PowerShell instance in our application, so we do not need to invoke an external PowerShell process to execute commands.

Let’s see how simple it is to execute a script.

This time we’ll create a simple echo.ps1 script:

'I am invoked by PowerShell class!'

Now let’s invoke this script using the PowerShell class:

PowerShell ps = PowerShell.Create();
ps.AddScript(@".\echo.ps1").Invoke();

We simply pass the path to our script to invoke our echo.ps1 script.

Let’s execute a simple command directly this time, we’ll use Get-Date to get the current date time:

using var ps = PowerShell.Create();
ps.AddCommand("Get-Date"); 
var processes = ps.Invoke(); 
Console.WriteLine(processes.First().ToString()); // 10/05/2023 19:12:34

Notice that we instantiate the PowerShell class in the using statement to dispose of it properly after. We create a PowerShell instance, add a command and invoke it.

To display the resulting objects to the console, we just use the First method and convert the result to a string.

As we can see, as long as we know the command in PowerShell, we can easily transform it and use it in C#. Let us try one more example where we start a process which is the notepad:

using var ps = PowerShell.Create();
ps.AddCommand("Start-Process").AddArgument("notepad");
ps.Invoke(); 

Notice that the PowerShell class allows method chaining. We use the AddArgument() method after the AddCommand() method.

By default, the PowerShell class uses the default runspace to run the script and execute commands. The default runspace represents the default execution environment for our commands and scripts.

Let’s see how to use a custom runspace.

Custom Runspace

Alongside security reasons, we might use a custom runspace for performance gains. This is because we limit the runspace to a specified subset of commands that we actually need. That means that a runspace loads only the commands that we specify.

First, we need to create an InitialSessionState object. This class allows us to define a set of elements that should be present when a SessionState is created. The SessionStateVariableEntry class allows us to create new session state variables.

A session state variable is a variable that persists for the entire session, similar to a global variable. We need it because it represents the set of allowed commands we can use in our runspace throughout the session. SessionState refers to the current configuration of a Windows PowerShell session or module.

Basically, the PowerShell session refers to the time from starting a host application that opens  PowerShell runspace up until it closes the runspace. We will apply all our restrictions to this InitialSessionState object:

InitialSessionState iss = InitialSessionState.Create();
var entry = new SessionStateVariableEntry("AllowedCommands", 
    new[] { "Get-Date" }, "List of allowed commands");
iss.Variables.Add(entry);

Here we allow only a single command to be used, the Get-Date command. 

Next, we need to import the Microsoft.PowerShell.Utility module that contains this command and apply the command to the InitialSessionState object:

var ms = new ModuleSpecification("Microsoft.PowerShell.Utility");
iss.ImportPSModule(new[] { ms });

var getDateCmdlet = new SessionStateCmdletEntry("Get-Date", 
    typeof(Microsoft.PowerShell.Commands.GetDateCommand), "");
iss.Commands.Add(getDateCmdlet);

Now we can create and run our runspace to use the configured InitialSessionState:

Runspace rs = RunspaceFactory.CreateRunspace(iss);
rs.Open();

All that remains now is to apply our custom runspace to the PowerShell instance and to try it out:

using var ps = PowerShell.Create();
ps.Runspace = rs;
ps.AddCommand("Get-Date");
var processes= ps.Invoke();
Console.WriteLine(processes.First().ToString()); // 10/05/2023 20:15:34    

As expected, we got the time and date printed on the console.

Lastly, let’s try to run the Start-Process command instead, which is not in our custom runspace. To better understand what is happening, we will create a method that returns true or false depending on the execution result of our command:

public bool StartProcess(string processName)
{
    try
    {
        using (var ps = PowerShell.Create())
        {
            ps.Runspace = rs;
            ps.AddCommand("Start-Process").AddArgument(processName);
            ps.Invoke();
            return true;
        }
    }
    catch (Exception)
    {
        return false;
    }
}

Next, let’s print the result to the console:

var processStarted = StartProcess("notepad");
if (!processStarted)
{
    Console.WriteLine("This is a custom runspace that can only run Get-Date command");
}

Our custom runspace does not recognize the command and throws an exception with the message. This shows we have successfully created a restricted runspace.

Conclusion

In this article, we reviewed two ways to execute a PowerShell script in C#. We first covered using the ProcessStartInfo class. Next, we focused on the PowerShell class from the PowerShell.SDK and the difference between this class and the ProcessStartInfo class. Lastly, we took a step further by looking at a custom runspace where we can restrict which commands can be executed.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!