Running PowerShell with C#

Introduction

Learn basics for working with Microsoft PowerShell within Visual Studio projects with a NuGet package or firing off a process within in .NET Core Framework projects using C# programming language.

All code samples shown are available in this repository.

Requirements

What is PowerShell?

PowerShell is a cross-platform task automation solution made up of a command-line shell, a scripting language, and a configuration management framework. PowerShell runs on Windows, Linux, and macOS.

As a scripting language, PowerShell is commonly used for automating the management of systems. It is also used to build, test, and deploy solutions, often in CI/CD environments. PowerShell is built on the .NET Common Language Runtime (CLR). All inputs and outputs are .NET objects. 

Although C# is a robust programming sitting on top of the .NET Framework there are times when running PowerShell commands from a process are faster than calling PowerShell commands from native NuGet PowerShell packages.
In some organizations, running PowerShell commands from a managed package may violate company polices while running the same commands from a process will not. 
A downside to running PowerShell commands from a process is more work when there is a need to run multiple commands spanning more than one line.

Getting started

Start with one-liners, example, get the IP address for the current machine. One a terminal or PowerShell window and type in Invoke-RestMethod ipinfo.io/ip and press enter.

The IP is shown

To translate this into code which runs in a Windows Form .NET Core project.

01.namespace ProcessingAndWait.Classes
02.{
03.    public class  PowerShellOperations
04.    {
05. 
06.        /// <summary>
07.        /// Get this computer's IP address synchronous 
08.        /// </summary>
09.        /// <returns>Task<string></returns>
10.        public static  async Task<string> GetIpAddressTask()
11.        {
12.            const string  fileName = "ipPower_Async.txt";
13. 
14. 
15.            if (File.Exists(fileName))
16.            {
17.                File.Delete(fileName);
18.            }
19. 
20.            var start = new  ProcessStartInfo
21.            {
22.                FileName = "powershell.exe",
23.                UseShellExecute = false,
24.                RedirectStandardOutput = true,
25.                Arguments = "Invoke-RestMethod ipinfo.io/ip",
26.                CreateNoWindow = true
27.            };
28. 
29. 
30.            using var process = Process.Start(start);
31.            using var reader = process.StandardOutput;
32. 
33.            process.EnableRaisingEvents = true;
34. 
35.            var ipAddressResult = reader.ReadToEnd();
36. 
37.            await File.WriteAllTextAsync(fileName, ipAddressResult);
38.            await process.WaitForExitAsync();
39. 
40.            return await File.ReadAllTextAsync(fileName);
41.        }
42. 
43. 
44.    }
45. 
46.}
  • fileName variable is where results are written.
  • ProcessStartInfo configures a process to get the IP address. In this example and those which following this keeps a window from appearing.
  • Process starts with results redirected to the file name in bullet 1.
  • Once done, the results are read into a string.

The caller assigns the IP address to a TextBox.

1.private  void  GetIpAddressVersion1Button_Click(object sender, EventArgs e)
2.{
3.    IpAddressTextBox1.Text = "";
4.    IpAddressTextBox1.Text = PowerShellOperations.GetIpAddressSync();
5.}

If for any reason the above code takes a long time to run (like from a slow computer) the above can run asynchronously.

01.public class  PowerShellOperations
02.{
03. 
04.    public static  async Task<string> GetIpAddressTask()
05.    {
06.        const string  fileName = "ipPower_Async.txt";
07. 
08. 
09.        if (File.Exists(fileName))
10.        {
11.            File.Delete(fileName);
12.        }
13. 
14.        var start = new  ProcessStartInfo
15.        {
16.            FileName = "powershell.exe",
17.            UseShellExecute = false,
18.            RedirectStandardOutput = true,
19.            Arguments = "Invoke-RestMethod ipinfo.io/ip",
20.            CreateNoWindow = true
21.        };
22. 
23. 
24.        using var process = Process.Start(start);
25.        using var reader = process.StandardOutput;
26. 
27.        process.EnableRaisingEvents = true;
28. 
29.        var ipAddressResult = await reader.ReadToEndAsync();
30. 
31.        await File.WriteAllTextAsync(fileName, ipAddressResult);
32.        await process.WaitForExitAsync();
33. 
34.        return await File.ReadAllTextAsync(fileName);
35.    }
36.}

Suppose more information is required, the following uses NuGet package Json.net and a container class to provide more details.

Container

01.using Newtonsoft.Json;
02. 
03.namespace ProcessingAndWait.Classes.Containers
04.{
05.    public class  IpItem
06.    {
07.        [JsonProperty("ip")]
08.        public string  Ip { get; set; }
09.        [JsonProperty("city")]
10.        public string  City { get; set; }
11.        [JsonProperty("country")]
12.        public string  Country { get; set; }
13.        [JsonProperty("loc")]
14.        public string  Location { get; set; }
15.        [JsonProperty("org")]
16.        public string  Org { get; set; }
17.        [JsonProperty("region")]
18.        public string  Region { get; set; }
19.        [JsonProperty("postal")]
20.        public string  Postal { get; set; }
21.        [JsonProperty("timezone")]
22.        public string  Timezone { get; set; }
23.        [JsonProperty("readme")]
24.        public string  Readme { get; set; }
25. 
26.        public string  Details => $"IP:{Ip}\nRegion: {Region}\nCountry: {Country}";
27.    }
28.}

JsonPropery provides an alias for each property as json is case sensitive while properties should be camel case. To obtain details use the following which indicates an output file.

01.public static  async Task<IpItem> GetIpAddressAsJsonTask()
02.{
03.    const string  fileName = "externalip.json";
04. 
05. 
06.    if (File.Exists(fileName))
07.    {
08.        File.Delete(fileName);
09.    }
10. 
11.    var start = new  ProcessStartInfo
12.    {
13.        FileName = "powershell.exe",
14.        UseShellExecute = false,
15.        RedirectStandardOutput = true,
16.        Arguments = "Invoke-RestMethod -uri https://ipinfo.io/json -outfile externalip.json",
17.        CreateNoWindow = true
18.    };
19. 
20. 
21.    using var process = Process.Start(start);
22.    using var reader = process.StandardOutput;
23. 
24.    process.EnableRaisingEvents = true;
25. 
26.    var ipAddressResult = await reader.ReadToEndAsync();
27.    if (File.Exists(fileName))
28.    {
29.        var json = File.ReadAllText(fileName);
30.        return JsonConvert.DeserializeObject<IpItem>(json);
31.    }
32. 
33.    return new  IpItem();
34.}

Returns, in this case several properties to keep things simple.

Obtaining services on the current machine. As with any language a PowerShell command can end up appearing complex, until one gets familiar with it which is best done not by writing commands in code but in a PowerShell window.

Here is code to get services.

01.public class  PowerShellOperations
02.{
03.    public static  async Task<List<ServiceItem>> GetServicesAsJson()
04.    {
05.        const string  fileName = "services.txt";
06. 
07.        if (File.Exists(fileName))
08.        {
09.            File.Delete(fileName);
10.        }
11. 
12.        var start = new  ProcessStartInfo
13.        {
14.            FileName = "powershell.exe",
15.            UseShellExecute = false,
16.            RedirectStandardOutput = true,
17.            Arguments = "Get-Service | Select-Object Name, DisplayName, @{ n='Status'; " + 
18.                        "e={ $_.Status.ToString() } }, @{ n='table'; e={ 'Status' } } | ConvertTo-Json",
19.            CreateNoWindow = true
20.        };
21. 
22.        using var process = Process.Start(start);
23.        using var reader = process.StandardOutput;
24. 
25.        process.EnableRaisingEvents = true;
26. 
27.        var fileContents = await reader.ReadToEndAsync();
28. 
29.        await File.WriteAllTextAsync(fileName, fileContents);
30.        await process.WaitForExitAsync();
31. 
32.        var json = await File.ReadAllTextAsync(fileName);
33. 
34.        return JsonSerializer.Deserialize<List<ServiceItem>>(json);
35. 
36.    }
37. 
38.}

Container class (Json aliasing excluded)

01.public class  ServiceItem
02.{
03.    public string  Name { get; set; }
04.    public string  DisplayName { get; set; }
05.    public string  Status { get; set; }
06.    public string  table { get; set; }
07.    public ServiceStartMode ServiceStartMode { get; set; }
08.    public override  string ToString() => Name;
09.    /// <summary>
10.    /// For adding items to a ListView
11.    /// </summary>
12.    /// <returns></returns>
13.    public string[] ItemArray() => new[] { Name, DisplayName, Status };
14.}

See the project for calling code, here is the output.

Perhaps another option is presenting services in a web page.

Code for the above

  • Named value tuple is used to return to the caller if the 
    • Operation was successful
    • An exception object for failure to complete the task.
  • ChromeLauncher.OpenLink(fileName); fires up a Chrome browser if installed.
01.public static  async Task<(bool, Exception)> GetServicesAsHtml()
02.{
03.    string fileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "service.html");
04. 
05.    if (File.Exists(fileName))
06.    {
07.        File.Delete(fileName);
08.    }
09. 
10.    var start = new  ProcessStartInfo
11.    {
12.        FileName = "powershell.exe",
13.        UseShellExecute = false,
14.        RedirectStandardOutput = true,
15.        Arguments = "Get-Service | Select-Object Name, DisplayName, @{ n='Status'; " + 
16.                    "e={ $_.Status.ToString() } }, @{ n='table'; e={ 'Status' } } | ConvertTo-html",
17.        CreateNoWindow = true
18.    };
19. 
20.    using var process = Process.Start(start);
21.    using var reader = process.StandardOutput;
22. 
23.    process.EnableRaisingEvents = true;
24. 
25.    var fileContents = await reader.ReadToEndAsync();
26. 
27.    await File.WriteAllTextAsync(fileName, fileContents);
28.    await process.WaitForExitAsync();
29. 
30.    try
31.    {
32.        ChromeLauncher.OpenLink(fileName);
33.        return (true, null);
34.    }
35.    catch (Exception ex)
36.    {
37.        return (false, ex);
38.    }
39. 
40.}

Asynchronous importance

All but one prior code sample has been asynchronous while prudent to run asynchronously there can be cases like querying Windows event logs. Even one day of reading logs can take 30 or more seconds. For this reason, consider making all calls asynchronous. Included in source code there is an example to obtain yesterday’s event log.  An exception would be asking for x of newest log entries e.g. 

1.Get-EventLog -LogName system -EntryType `Error` -Newest 50

While running the project ProcessingAndWait click on each button without waiting and then move the form on the screen. This is because all processing keeps the form responsive.
Writing code that is not responsive indicates poorly written code and with that a coder/developer's clients will lose respect for coders/developers.
 

Working with PowerShell packages

Before starting make sure to install the proper package in your project.

In Package Manager Console

PM> Install-Package Microsoft.PowerShell.SDK -Version 7.2.0-preview.4, always pick the current version or as in this case a preview package was used.

If the .NET Framework classic package is selected the package manager will attempt to install it but will fail and rollback the installation.

Simple code sample

Here are several code sample for getting acquainted with managed code for PowerShell. These are included in the included source code.

01.public class  PowerShellOperations
02.{
03.    public delegate  void OnProcess(ProcessItem sender);
04.    /// <summary>
05.    /// Raised event when invoking a pipeline
06.    /// </summary>
07.    public static  event OnProcess ProcessItemHandler;
08. 
09.    /// <summary>
10.    /// Synchronous processing
11.    /// </summary>
12.    public static  void Example1()
13.    {
14.        var ps = PowerShell.Create().AddCommand("Get-Process");
15.        IAsyncResult pipeAsyncResult = ps.BeginInvoke();
16. 
17.        foreach (PSObject result in ps.EndInvoke(pipeAsyncResult))
18.        {
19. 
20.            ProcessItemHandler?.Invoke(new ProcessItem()
21.            {
22.                Value = $"{result.Members["ProcessName"].Value,-20}{result.Members["Id"].Value}"
23.            });
24.             
25.        }
26.    }
27. 
28.    /// <summary>
29.    /// Asynchronous processing
30.    /// </summary>
31.    /// <returns></returns>
32.    public static  async Task Example2Task()
33.    {
34.        await Task.Run(async () =>
35.        {
36.            await Task.Delay(0);
37.            var ps = PowerShell.Create().AddCommand("Get-Process");
38. 
39.            var pipeAsyncResult = ps.BeginInvoke();
40. 
41.            foreach (var result in ps.EndInvoke(pipeAsyncResult))
42.            {
43.                 
44.                ProcessItemHandler?.Invoke(new ProcessItem()
45.                {
46.                    Value = $"{result.Members["ProcessName"].Value,-20}{result.Members["Id"].Value}"
47.                });
48. 
49.            }
50. 
51.        });
52. 
53.    }
54. 
55.    static readonly  string ScriptPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "runner.ps1");
56.    /// <summary>
57.    /// Run a PowerShell script
58.    /// </summary>
59.    /// <returns></returns>
60.    public static  string RunScript()
61.    {
62.        // create PowerShell runspace
63.        var runspace = RunspaceFactory.CreateRunspace();
64. 
65.        // open it
66.        runspace.Open();
67. 
68.        // create a pipeline and feed it the script text
69.        Pipeline pipeline = runspace.CreatePipeline();
70.        pipeline.Commands.AddScript(ScriptPath);
71.         
72.        pipeline.Commands.Add("Out-String");
73. 
74.        // execute the script
75.        var results = pipeline.Invoke();
76. 
77.        // close the runspace
78.        runspace.Close();
79. 
80.        // convert the script result into a single string
81.        StringBuilder stringBuilder = new();
82.         
83.        foreach (var psObject in results)
84.        {
85.            stringBuilder.AppendLine(psObject.ToString());
86.        }
87. 
88.        return stringBuilder.ToString();
89.    }
90.}

Practice

Mentioned prior, don't attempt to try out commands in code. First run commands in a PowerShell window. Even with this thee may be cases a command or set of commands may fail if a policy prohibits running PowerShell scripts.

See the following markdown file for common commands to try out.

Summary

With information presented a developer can get started to proficiency writing simple to complex PowerShell commands in Visual Studio using C#.

See also

Getting started with PowerShell
Azure PowerShell documentation

Source