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 the repository listed at the end of the article.

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 proohibits 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

Source