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:
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:
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.
public static bool RunPS(PowerShell ps, string psCommand, out Collection<PSObject> outs)
{
outs = new Collection<PSObject>();
HasError = false;
ps.Commands.Clear();
ps.Streams.ClearStreams();
ps.AddScript(psCommand);
outs = ExecutePS(ps);
return !HasError;
}
public static bool RunPS(PowerShell ps, string psCommand,
out Collection<PSObject> outs, params ParameterPair[] parameters)
{
outs = new Collection<PSObject>();
HasError = false;
if (!psCommand.Contains(' '))
{
ps.Commands.Clear();
ps.Streams.ClearStreams();
ps.AddCommand(psCommand);
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);
}
outs = ExecutePS(ps);
}
else LastException = new Exception
("E1007:Only one command with no parameters is allowed");
return !HasError;
}
private static Collection<PSObject> ExecutePS(PowerShell ps)
{
Collection<PSObject> retVal = new Collection<PSObject>();
try
{
retVal = ps.Invoke();
if (ps.Streams.Error.Count > 0)
{
LastException = new Exception("E0002:Errors were detected during execution");
LastErrors = new PSDataCollection<ErrorRecord>(ps.Streams.Error);
}
}
catch (Exception ex)
{
LastException = new Exception("E0001:" + ex.Message);
}
return retVal;
}
How to Use a Class Prepared in Such a Way
At the beginning, we create an empty collection for the results:
Collection<PSObject> Results = new Collection<PSObject>();
Then we need to open the individual PowerShell environment:
PowerShell ps = PowerShell.Create();
Then try to execute the script:
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:
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#
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation;
namespace PS
{
public static class Basic
{
public static Exception LastException = null;
public static PSDataCollection<ErrorRecord> LastErrors =
new PSDataCollection<ErrorRecord>();
public static bool HasError
{
get
{
return LastException != null;
}
set
{
if(!value)
{
LastException = null;
LastErrors = new PSDataCollection<ErrorRecord>();
}
}
}
public static int ErrorCode
{
get
{
if (HasError) return int.Parse(LastException.Message.Substring(1, 4));
return 0;
}
}
public static bool RunPS
(PowerShell ps, string psCommand, out Collection<PSObject> outs)
{
outs = new Collection<PSObject>();
HasError = false;
ps.Commands.Clear();
ps.Streams.ClearStreams();
ps.AddScript(psCommand);
outs = ExecutePS(ps);
return !HasError;
}
public static bool RunPS(PowerShell ps, string psCommand,
out Collection<PSObject> outs, params ParameterPair[] parameters)
{
outs = new Collection<PSObject>();
HasError = false;
if (!psCommand.Contains(' '))
{
ps.Commands.Clear();
ps.Streams.ClearStreams();
ps.AddCommand(psCommand);
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);
}
outs = ExecutePS(ps);
}
else LastException = new Exception("E1007:Only one command
with no parameters is allowed");
return !HasError;
}
private static Collection<PSObject> ExecutePS(PowerShell ps)
{
Collection<PSObject> retVal = new Collection<PSObject>();
try
{
retVal = ps.Invoke();
if (ps.Streams.Error.Count > 0)
{
LastException = new Exception
("E0002:Errors were detected during execution");
LastErrors = new PSDataCollection<ErrorRecord>(ps.Streams.Error);
}
}
catch (Exception ex)
{
LastException = new Exception("E0001:" + ex.Message);
}
return retVal;
}
}
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