Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / PowerShell

One More Solution to Calling PowerShell from C#

4.88/5 (5 votes)
26 Nov 2021CPOL4 min read 13.6K  
Basic PowerShell handler C# class with an explanation
What elements can be used to build the simplest class that supports PowerShell from C# in the .NET environment. Sequence of steps when invoking a command in PowerShell. Handling of results and errors.

Introduction

PowerShell support from C# is a bit of a hassle. Here, I present a simple solution without delving into the depths of the environment. This article is intended to be the basis of the cycle about supporting various Microsoft environments using PowerShell invoked from a program written in C#.

Prerequisites

All examples were created, compiled and tested in MS Visual Studio Community 2019 in a Windows 10 Pro 21H2 19044.1387 environment.

For proper compilation, it is required to install the NuGet package: Microsoft.PowerShell.3.ReferenceAssemblies.

Requirements for Methods in Custom Extension Libraries

You can use methods and classes already defined in the respective namespaces, but I have included simple examples of solutions for your understanding and clarity. You can certainly write them differently (in a simpler or clearer way), but let's not bend too much into it.

Below is the method that returns true when the string value is empty or contains only white space:

C#
public static bool IsNullEmptyOrWhite(this string sValue) {...}

Class defining the PowerShell parameter in the form Name/Value. It can be replaced with any convenient dictionary class:

C#
public class ParameterPair
{
    public string Name { get; set; } = string.Empty;
    public object Value { get; set; } = null;
}

Using the Code

Execution of a Command in the PowerShell Environment

Of course, there are many ways to open PowerShell, issue a command within it, and download its results. I limited myself to two:

  • Calling a script where all commands and their data must be presented as one line of text
  • Cmdlet calling where we can only give one command at a time and its parameters are passed as Name/Value pairs, where values can be of any type

For this reason, two RunPS methods with different parameter sets are defined, which, after proper parameter processing, use the uniform ExecutePS method.

C#
/// <summary>
/// Basic method of calling PowerShell a script where all commands 
/// and their data must be presented as one line of text
/// </summary>
/// <param name="ps">PowerShell environment</param>
/// <param name="psCommand">A single line of text 
/// containing commands and their parameters (in text format)</param>
/// <param name="outs">A collection of objects that contains the feedback</param>
/// <returns>The method returns true when executed correctly 
/// and false when some errors have occurred</returns>
public static bool RunPS(PowerShell ps, string psCommand, out Collection<PSObject> outs)
{
    //Programmer's Commandment I: Remember to reset your variables
    outs = new Collection<PSObject>();
    HasError = false;
    
    //Cleanup of PowerShell also due to commandment I
    ps.Commands.Clear();
    ps.Streams.ClearStreams();
    
    //We put the script into the PowerShell environment 
    //along with all commands and their parameters
    ps.AddScript(psCommand);
    
    //We are trying to execute our command
    outs = ExecutePS(ps);
    
    //The method returns true when executed correctly and false 
    //when some errors have occurred
    return !HasError;
}

/// <summary>
/// Method 2 cmdlet call where we can only give one command at a time
/// and its parameters are passed as Name/Value pairs,
/// where values can be of any type
/// </summary>
/// <param name="ps">PowerShell environment</param>
/// <param name="psCommand">Single command with no parameters</param>
/// <param name="outs">A collection of objects that contains the feedback</param>
/// <param name="parameters">A collection of parameter pairs
/// in the form Name/Value</param>
/// <returns>The method returns true when executed correctly
/// and false when some errors have occurred</returns>
public static bool RunPS(PowerShell ps, string psCommand,
       out Collection<PSObject> outs, params ParameterPair[] parameters)
{
    //Programmer's Commandment I: Remember to reset your variables
    outs = new Collection<PSObject>();
    HasError = false;        
    
    if (!psCommand.Contains(' '))
    {
        //Cleanup of PowerShell also due to commandment I
        ps.Commands.Clear();
        ps.Streams.ClearStreams();
        
        //We put a single command into the PowerShell environment
        ps.AddCommand(psCommand);
        
        //Now we enter the command parameters in the form of Name/Value pairs
        foreach (ParameterPair PP in parameters)
        {
            if (PP.Name.IsNullEmptyOrWhite())
            {
                LastException = new Exception("E1008:Parameter cannot be unnamed");
                return false;
            }
            if (PP.Value == null) ps.AddParameter(PP.Name);
            else ps.AddParameter(PP.Name, PP.Value);
        }
        
        //We are trying to execute our command
        outs = ExecutePS(ps);
    }
    //And here we have a special exception 
    //if we tried to apply the method not to a single command
    else LastException = new Exception
    ("E1007:Only one command with no parameters is allowed");
    
    //The method returns true when executed correctly 
    //and false when some errors have occurred
    return !HasError;
}

/// <summary>
/// Internal method in which we try to execute a script or command with parameters
/// This method does not need to return a fixed value 
/// that indicates whether or not the execution succeeded,
/// since the parent methods use the principal properties of the class set in it.
/// </summary>
/// <param name="ps">PowerShell environment</param>
/// <returns>A collection of objects that contains the feedback</returns>
private static Collection<PSObject> ExecutePS(PowerShell ps)
{
    Collection<PSObject> retVal = new Collection<PSObject>();
    
    //We are trying to execute our script
    try
    {
        retVal = ps.Invoke();
        
        // ps.HadErrors !!! NO!
        // The PowerShell environment has a special property that
        // indicates in the assumption whether errors have occurred
        // unfortunately, most often I have found that despite errors,
        // its value is false or vice versa,
        // in the absence of errors, it pointed to the truth.
        
        //Therefore, we check the fact that errors have occurred,
        //using the error counter in PowerShell.Streams
        if (ps.Streams.Error.Count > 0) //czy są błędy wykonania
        {
            //We create another general exception, but we do not raise it.
            LastException = new Exception("E0002:Errors were detected during execution");
            
            //And we write runtime errors to the LastErrors collection
            LastErrors = new PSDataCollection<ErrorRecord>(ps.Streams.Error);
        }
    }
    
    //We catch script execution errors and exceptions
    catch (Exception ex)
    {
        //And if they do, we create a new general exception but don't raise it
        LastException = new Exception("E0001:" + ex.Message);
    }
    
    //Returns a collection of results
    return retVal;
}

How to Use a Class Prepared in Such a Way

At the beginning, we create an empty collection for the results:

C#
Collection<PSObject> Results = new Collection<PSObject>();

Then we need to open the individual PowerShell environment:

C#
PowerShell ps = PowerShell.Create();

Then try to execute the script:

C#
if (PS.Basic.RunPS(ps, "Get-Service | 
    Where-Object {$_.canpauseandcontinue -eq \"True\"}", out Results))
{ Interpreting the results… }
else
{ Interpretation of errors… }

or a command with parameters:

C#
if (PS.Basic.RunPS(ps, "Get-Service", out Results,
   new ParameterPair { Name = "Name", Value = "Spooler"}
))
{ Interpreting the results… }
else
{ Interpretation of errors… }

The next step is to interpret the results:

In each case, we review the collection of Collection<PSObject> Results.

For example, for the script "Get-Service ...", the collection consists of objects of the base type (Results [0] .BaseObject.GetType()) ServiceController, which, among other things, has the property "name". We can read it with Results [0] .Properties ["name"]. Value.

The interpretation of the results comes down to reviewing the Results and retrieving the values of the appropriate properties of interest to us.

Or the interpretation of errors:

The base class has several properties and variables to handle when an error occurs when trying to execute a PowerShell command.

Errors From Incorrect Preparation to Execute the Command

These errors can come from incorrectly preparing Powershell before executing the command. For example, we will forget to open this environment by passing an empty ps. Or, instead of the correct script/command, we'll pass some syntax nonsense.

In this case, when trying to call ps.Invoke (); there is an exception in the description of which you can find the cause of the error. In the base class, the LastException variable is then defined with the message "E0001:" + ex.Message (i.e., the exception description preceded by the code "E0001").
During the error interpretation phase, you can check whether such an error has occurred by checking the value of the "ErrorCode" property (PS.Basic.ErrorCode == 1) and then using the description in LastException.Message to perform a more detailed error handling.

Command Execution Errors

We can also get errors when executing a fully valid command or script. For example, when we specify the Identity of an object, the value of which does not have any object in the scope of the visibility of the PowerShell environment. Then we will get the error "not found" or "wrong name", but just trying to execute the command will not cause an exception.

We can find such errors in the collection PSDataCollection<ErrorRecord> LastErrors.

The introduced error handling model in the presented class will result in a new exception in LastException with the description in the form: "E0002: Errors were detected during execution". After checking with "ErrorCode" (PS.Basic.ErrorCode == 2), we can read subsequent errors from the collection, and determine their causes based on the exception description in LastErrors [0] .Exception.Message. As with LastException, now on this basis, it would be necessary to perform a more detailed error handling.

Full Code of the PowerShell Handler Class from under C#

C#
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation;

namespace PS
{
    public static class Basic
    {
        /// <summary>
        /// The last exception that occurred in the PS.Basic class
        /// </summary>
        public static Exception LastException = null;

        /// <summary>
        /// Collection of PowerShell runtime errors
        /// </summary>
        public static PSDataCollection<ErrorRecord> LastErrors = 
                                                    new PSDataCollection<ErrorRecord>();

        /// <summary>
        /// Auxiliary Property that helps to check if there was an error and 
        /// resets the error state
        /// </summary>
        public static bool HasError
        {
            get
            {
                return LastException != null;
            }
            set
            {
                if(!value)
                {
                    LastException = null;
                    LastErrors = new PSDataCollection<ErrorRecord>();
                }
            }
        }

        /// <summary>
        /// A helper Property to help you get the error code
        /// </summary>
        public static int ErrorCode
        {
            get
            {
                if (HasError) return int.Parse(LastException.Message.Substring(1, 4));
                return 0;
            }
        }

        /// <summary>
        /// Basic method of calling PowerShell a script where all commands 
        /// and their data must be presented as one line of text
        /// </summary>
        /// <param name="ps">PowerShell environment</param>
        /// <param name="psCommand">A single line of text containing commands 
        /// and their parameters (in text format)</param>
        /// <param name="outs">A collection of objects that contains the feedback</param>
        /// <returns>The method returns true when executed correctly 
        /// and false when some errors have occurred</returns>
        public static bool RunPS
               (PowerShell ps, string psCommand, out Collection<PSObject> outs)
        {
            //Programmer's Commandment I: Remember to reset your variables
            outs = new Collection<PSObject>();
            HasError = false;

            //Cleanup of PowerShell also due to commandment I
            ps.Commands.Clear();
            ps.Streams.ClearStreams();

            //We put the script into the PowerShell environment 
            //along with all commands and their parameters
            ps.AddScript(psCommand);

            //We are trying to execute our command
            outs = ExecutePS(ps);

            //The method returns true when executed correctly and false 
            //when some errors have occurred
            return !HasError;
        }

        /// <summary>
        /// Method 2 cmdlet call where we can only give one command 
        /// at a time and its parameters are passed as Name/Value pairs,
        /// where values can be of any type
        /// </summary>
        /// <param name="ps">PowerShell environment</param>
        /// <param name="psCommand">Single command with no parameters</param>
        /// <param name="outs">A collection of objects that contains the feedback</param>
        /// <param name="parameters">A collection of parameter pairs 
        /// in the form Name/Value</param>
        /// <returns>The method returns true when executed correctly 
        /// and false when some errors have occurred</returns>
        public static bool RunPS(PowerShell ps, string psCommand, 
               out Collection<PSObject> outs, params ParameterPair[] parameters)
        {
            //Programmer's Commandment I: Remember to reset your variables
            outs = new Collection<PSObject>();
            HasError = false;
           
            if (!psCommand.Contains(' '))
            {
                //Cleanup of PowerShell also due to commandment I
                ps.Commands.Clear();
                ps.Streams.ClearStreams();

                //We put a single command into the PowerShell environment
                ps.AddCommand(psCommand);

                //Now we enter the command parameters in the form of Name/Value pairs
                foreach (ParameterPair PP in parameters)
                {
                    if (PP.Name.IsNullEmptyOrWhite())
                    {
                        LastException = new Exception("E1008:Parameter cannot be unnamed");
                        return false;
                    }

                    if (PP.Value == null) ps.AddParameter(PP.Name);
                    else ps.AddParameter(PP.Name, PP.Value);
                }

                //We are trying to execute our command
                outs = ExecutePS(ps);
            }
            //And here we have a special exception if we tried 
            //to apply the method not to a single command
            else LastException = new Exception("E1007:Only one command 
                                                with no parameters is allowed");

            //The method returns true when executed correctly and false 
            //when some errors have occurred
            return !HasError;
        }

        /// <summary>
        /// Internal method in which we try to execute a script or command with parameters
        /// This method does not need to return a fixed value that indicates 
        /// whether or not the execution succeeded,
        /// since the parent methods use the principal properties of the class set in it.
        /// </summary>
        /// <param name="ps">PowerShell environment</param>
        /// <returns>A collection of objects that contains the feedback</returns>
        private static Collection<PSObject> ExecutePS(PowerShell ps)
        {
            Collection<PSObject> retVal = new Collection<PSObject>();

            //We are trying to execute our script
            try
            {
                retVal = ps.Invoke();

                // ps.HadErrors !!! NO!
                // The PowerShell environment has a special property 
                // that indicates in the assumption whether errors have occurred
                // unfortunately, most often, I have found that despite errors 
                // its value is false or vice versa,
                // in the absence of errors, it pointed to the truth.

                // Therefore, we check the fact that errors have occurred, 
                // using the error counter in PowerShell.Streams
                if (ps.Streams.Error.Count > 0) //czy są błędy wykonania
                {
                    //We create another general exception, but we do not raise it.
                    LastException = new Exception
                                    ("E0002:Errors were detected during execution");

                    //And we write runtime errors to the LastErrors collection
                    LastErrors = new PSDataCollection<ErrorRecord>(ps.Streams.Error);
                }
            }
            //We catch script execution errors and exceptions
            catch (Exception ex)
            {
                //And if they do, we create a new general exception but don't raise it
                LastException = new Exception("E0001:" + ex.Message);
            }

            //Returns a collection of results
            return retVal;
        }
    }

    /// <summary>
    /// Class defining the PowerShell parameter in the form Name/Value.
    /// it can be replaced with any convenient dictionary class
    /// </summary>
    public class ParameterPair
    {
        public string Name { get; set; } = string.Empty;

        public object Value { get; set; } = null;
    }
}

History

  • 26th November, 2021: First version of the article

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)