How to Run PowerShell from C# (CSharp) and Receive the Results as Objects

· · C#, PowerShell, Windows, .NET, Automation, Legacy Asset Reuse

Situations where you want to run PowerShell from C# come up all the time in business applications and internal tools. For example:

  • Retrieving the list of Windows services
  • Inspecting processes and event logs
  • Calling existing PowerShell scripts from a C# application
  • Running PowerShell commands from a small GUI tool for administrators
  • Gradually absorbing existing PowerShell automation assets into a .NET application

If you just need to run something, launching powershell.exe or pwsh.exe as an external process and reading standard output as a string does work. But that approach loses the very thing that makes PowerShell good: the object pipeline.

PowerShell results are not really just text. The output of Get-Process is process objects; the output of Get-Service is service objects. If the C# side can receive that structure intact, string parsing becomes unnecessary and the whole process becomes considerably safer.

This article walks through the basics of running PowerShell from C# and receiving the results as PSObject.

The code in this article is published on GitHub as a complete buildable, runnable sample set (a library with the execution wrapper and conversion logic, a console demo that demonstrates each section of the article, and unit tests verifying PSObject handling and error processing).

csharp-run-powershell-receive-objects - komurasoft-blog-samples (GitHub)

1. Use the PowerShell SDK, Not an External Process

There are broadly two ways to invoke PowerShell from C#.

Approach Characteristics Suited for
Launch powershell.exe / pwsh.exe via ProcessStartInfo Read stdout and stderr as strings Simple execution of existing batches, jobs that just leave logs
Use System.Management.Automation.PowerShell Receive results as PSObject Processing results in C#, admin tools, business applications

This article covers the latter. With System.Management.Automation.PowerShell, you can assemble and run a PowerShell pipeline from C# code. The key point is that the return value is not a string — it is fundamentally a Collection<PSObject>.

In other words, the mental model is:

Run a PowerShell command
  ↓
Receive the results as a collection of PSObject
  ↓
Extract values via BaseObject or Properties
  ↓
Convert to C# DTOs / records / classes as needed

The point is to treat PowerShell output as objects from the start, rather than decomposing it as strings.

2. Environment Assumptions

This article uses a .NET 8 console application as the example. The PowerShell SDK targets different versions of .NET depending on its own version, so choose one matching your project’s target framework.

As of June 2026, this is a useful way to think about it:

C# application target Example PowerShell SDK Notes
.NET 8 Microsoft.PowerShell.SDK 7.4 series Easy to use in .NET 8 apps
.NET 10 Microsoft.PowerShell.SDK 7.6 series Candidate when using a newer PowerShell SDK
.NET Framework Microsoft.PowerShell.5.1.ReferenceAssemblies For Windows PowerShell 5.1; for new development, confirm your requirements

Here we use Microsoft.PowerShell.SDK 7.4.16 as the .NET 8 example.

dotnet new console -n PowerShellObjectSample
cd PowerShellObjectSample
dotnet add package Microsoft.PowerShell.SDK --version 7.4.16

The .csproj ends up looking like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.PowerShell.SDK" Version="7.4.16" />
  </ItemGroup>

</Project>

We recommend pinning the version. The PowerShell SDK is convenient, but it is affected by the application’s runtime environment, the target .NET, and PowerShell module compatibility. For business applications, explicitly recording the version you verified is safer than just shipping “the latest version that happened to work on the dev machine.”

3. Minimal Code: Run PowerShell and Receive PSObject

First, let’s retrieve the current C# application’s own process via PowerShell.

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Management.Automation;

int currentProcessId = Environment.ProcessId;

using PowerShell ps = PowerShell.Create();

Collection<PSObject> results = ps
    .AddCommand("Get-Process")
    .AddParameter("Id", currentProcessId)
    .Invoke();

foreach (PSObject item in results)
{
    Console.WriteLine($"PSObject type: {item.GetType().FullName}");
    Console.WriteLine($"BaseObject type: {item.BaseObject.GetType().FullName}");

    if (item.BaseObject is Process process)
    {
        Console.WriteLine($"Id: {process.Id}");
        Console.WriteLine($"Name: {process.ProcessName}");
        Console.WriteLine($"Memory: {process.WorkingSet64:N0} bytes");
    }
}

There are three things to take away here: PowerShell.Create() creates the PowerShell execution object; AddCommand("Get-Process") and AddParameter("Id", currentProcessId) assemble the command and its parameters; and the return value of Invoke() is a Collection<PSObject>.

PSObject is a wrapper around the values PowerShell outputs. To see the original .NET object inside, look at BaseObject. In this example, the content of the Get-Process result can be extracted as a System.Diagnostics.Process.

4. Choosing Between BaseObject and Properties

When handling PowerShell results in C#, the first thing you will hesitate over is these two:

item.BaseObject
item.Properties["Name"]?.Value

Here is a guideline for which to use:

Extraction method When to use it
BaseObject When you want to use the original .NET object PowerShell returned, as is
Properties["..."] When you want to extract columns created with Select-Object or [pscustomobject]

When you run a command like Get-Process directly, BaseObject may contain the original .NET object. On the other hand, when the PowerShell side shapes columns with Select-Object, the results usually come back as PowerShell custom objects. In that case, retrieving values by column name from Properties is the natural approach.

5. Reading Select-Object Results in C#

In practice, you rarely need every property PowerShell returns. To pass only the necessary columns to the C# side, use Select-Object in the PowerShell pipeline.

using System.Collections.ObjectModel;
using System.Globalization;
using System.Management.Automation;

using PowerShell ps = PowerShell.Create();

Collection<PSObject> rows = ps
    .AddCommand("Get-Process")
    .AddCommand("Sort-Object")
        .AddParameter("Property", "CPU")
        .AddParameter("Descending", true)
    .AddCommand("Select-Object")
        .AddParameter("First", 10)
        .AddParameter("Property", new[] { "Name", "Id", "CPU", "WorkingSet" })
    .Invoke();

foreach (PSObject row in rows)
{
    string name = Convert.ToString(row.Properties["Name"]?.Value, CultureInfo.InvariantCulture) ?? "";
    int id = Convert.ToInt32(row.Properties["Id"]?.Value, CultureInfo.InvariantCulture);
    double? cpu = row.Properties["CPU"]?.Value is null
        ? null
        : Convert.ToDouble(row.Properties["CPU"]!.Value, CultureInfo.InvariantCulture);
    long workingSet = Convert.ToInt64(row.Properties["WorkingSet"]?.Value, CultureInfo.InvariantCulture);

    Console.WriteLine($"{id}: {name}, CPU={cpu}, WorkingSet={workingSet:N0}");
}

This code corresponds to the following pipeline in PowerShell:

Get-Process |
  Sort-Object -Property CPU -Descending |
  Select-Object -First 10 -Property Name, Id, CPU, WorkingSet

From the C# side, calling AddCommand repeatedly builds the PowerShell pipeline:

.AddCommand("Get-Process")
.AddCommand("Sort-Object")
.AddCommand("Select-Object")

Written this way, the output of each command is passed to the next.

After narrowing columns with Select-Object, you extract values by column name, as in row.Properties["Name"]?.Value.

6. Converting to a C# record

If you pass PSObject around the whole application, downstream code becomes too dependent on PowerShell. For display or business logic, converting to C#-side types makes things easier to work with.

For example, convert process information into this record:

public sealed record ProcessSummary(
    string Name,
    int Id,
    double? Cpu,
    long WorkingSet);

Splitting out the conversion logic like this keeps things tidy:

using System.Globalization;
using System.Management.Automation;

static ProcessSummary ToProcessSummary(PSObject row)
{
    string name = GetString(row, "Name");
    int id = GetInt32(row, "Id");
    double? cpu = GetNullableDouble(row, "CPU");
    long workingSet = GetInt64(row, "WorkingSet");

    return new ProcessSummary(name, id, cpu, workingSet);
}

static string GetString(PSObject row, string propertyName)
{
    return Convert.ToString(row.Properties[propertyName]?.Value, CultureInfo.InvariantCulture) ?? "";
}

static int GetInt32(PSObject row, string propertyName)
{
    return Convert.ToInt32(row.Properties[propertyName]?.Value, CultureInfo.InvariantCulture);
}

static long GetInt64(PSObject row, string propertyName)
{
    return Convert.ToInt64(row.Properties[propertyName]?.Value, CultureInfo.InvariantCulture);
}

static double? GetNullableDouble(PSObject row, string propertyName)
{
    object? value = row.Properties[propertyName]?.Value;
    return value is null ? null : Convert.ToDouble(value, CultureInfo.InvariantCulture);
}

The caller then looks like this:

List<ProcessSummary> processes = rows
    .Select(ToProcessSummary)
    .ToList();

foreach (ProcessSummary process in processes)
{
    Console.WriteLine($"{process.Id}: {process.Name}");
}

Handle PSObject at the boundary with PowerShell, and convert to ordinary C# types like ProcessSummary inside the application.

With this separation in place, changing the PowerShell command later keeps the blast radius small.

7. Returning PSCustomObject Makes the C# Side Easier

When you want the PowerShell side to return several values together, [pscustomobject] is convenient.

using System.Collections.ObjectModel;
using System.Management.Automation;

string script = @"
[pscustomobject]@{
    MachineName       = [System.Environment]::MachineName
    PowerShellVersion = $PSVersionTable.PSVersion.ToString()
    CurrentDirectory  = (Get-Location).Path
}
";

using PowerShell ps = PowerShell.Create();

Collection<PSObject> rows = ps
    .AddScript(script, useLocalScope: true)
    .Invoke();

foreach (PSObject row in rows)
{
    Console.WriteLine($"MachineName: {row.Properties["MachineName"]?.Value}");
    Console.WriteLine($"PowerShell:  {row.Properties["PowerShellVersion"]?.Value}");
    Console.WriteLine($"Directory:   {row.Properties["CurrentDirectory"]?.Value}");
}

If a PowerShell script returns a [pscustomobject] at the end, the C# side can extract values by name from Properties. This is considerably safer than returning a complicated string and splitting it on the C# side.

An example to avoid is output like this:

"$MachineName,$PowerShellVersion,$CurrentDirectory"

This approach looks easy, but it breaks the moment a value contains a comma or a newline.

Have PowerShell return objects, and read them as properties in C#. With this shape, adding columns later is easy to accommodate.

8. Never Embed User Input Directly into AddScript

Even when using the PowerShell SDK, assembling scripts as strings is dangerous. For example, code like this should be avoided:

// Example to avoid
string userInputPath = GetPathFromUser();
string script = $"Get-ChildItem -Path '{userInputPath}'";

using PowerShell ps = PowerShell.Create();
ps.AddScript(script).Invoke();

Written this way, user input can be interpreted as PowerShell code. When passing values to PowerShell commands, use AddCommand and AddParameter wherever possible.

string userInputPath = GetPathFromUser();

using PowerShell ps = PowerShell.Create();

Collection<PSObject> files = ps
    .AddCommand("Get-ChildItem")
    .AddParameter("Path", userInputPath)
    .AddParameter("File", true)
    .Invoke();

Values passed via AddParameter are treated as parameter values, not concatenated into a PowerShell code string.

In practice, this is a sensible division:

Style When to use
AddCommand / AddParameter When you want to assemble commands safely from the C# side
AddScript When running fixed short scripts or loading existing scripts
AddScript with string concatenation Avoid as a rule; if used, validate and escape inputs with great care

Embedding PowerShell in C# gives the application powerful operational capabilities. That convenience comes with one line you must not cross: never turn user input directly into script.

9. Format-Table Is for the Final Screen Display — Don’t Use It Before Passing to C#

When you want to receive PowerShell results as objects in C#, you basically do not use Format-Table or Format-List.

For example, this PowerShell is convenient for a human looking at a screen:

Get-Service | Format-Table Name, Status

But if you apply Format-Table before handing results to C#, what you receive is not service objects — it is formatting metadata for display. If you want to work with the data in C#, use Select-Object.

Get-Service | Select-Object Name, Status

Written from C#, it looks like this:

using PowerShell ps = PowerShell.Create();

Collection<PSObject> services = ps
    .AddCommand("Get-Service")
    .AddCommand("Select-Object")
        .AddParameter("Property", new[] { "Name", "Status" })
    .Invoke();

The idea is simple:

Just making it readable on screen → Format-Table / Format-List
Feeding downstream processing in C# → Select-Object / PSCustomObject

This is true when using PowerShell on its own too, but it becomes especially important when integrating with C#.

10. Receiving Errors

In PowerShell, output and errors are separate streams. If you only look at the return value of Invoke(), you can miss errors. The basic form is:

using System.Management.Automation;

using PowerShell ps = PowerShell.Create();

Collection<PSObject> output = ps
    .AddCommand("Get-Item")
    .AddParameter("Path", @"C:\no-such-file.txt")
    .Invoke();

if (ps.HadErrors)
{
    foreach (ErrorRecord error in ps.Streams.Error)
    {
        Console.WriteLine($"Error: {error.Exception.Message}");
        Console.WriteLine($"Category: {error.CategoryInfo.Category}");
        Console.WriteLine($"Target: {error.TargetObject}");
    }
}

PowerShell cmdlets produce both terminating errors and non-terminating errors. If you want to handle them as exceptions on the C# side, one approach is to specify Stop for ErrorAction.

using System.Management.Automation;

try
{
    using PowerShell ps = PowerShell.Create();

    Collection<PSObject> output = ps
        .AddCommand("Get-Item")
        .AddParameter("Path", @"C:\no-such-file.txt")
        .AddParameter("ErrorAction", "Stop")
        .Invoke();
}
catch (RuntimeException ex)
{
    Console.WriteLine($"PowerShell failed: {ex.Message}");
}

Which is better depends on the nature of the application. For an admin tool where you want the listing even if parts fail, collecting the error stream and showing it on screen fits better. If a failure should stop the whole operation, treating it as an exception via ErrorAction Stop is clearer.

11. Build a Small Execution Wrapper

In an application that calls PowerShell repeatedly, writing the same error handling every time gets messy. A simple wrapper helps.

using System.Management.Automation;

public sealed record PowerShellRunResult(
    IReadOnlyList<PSObject> Output,
    IReadOnlyList<ErrorRecord> Errors);

public static class PowerShellRunner
{
    public static PowerShellRunResult Run(Action<PowerShell> build)
    {
        using PowerShell ps = PowerShell.Create();

        build(ps);

        List<PSObject> output;

        try
        {
            output = ps.Invoke().ToList();
        }
        catch (RuntimeException ex)
        {
            throw new InvalidOperationException($"PowerShell execution failed: {ex.Message}", ex);
        }

        return new PowerShellRunResult(
            Output: output,
            Errors: ps.Streams.Error.ToList());
    }
}

The caller can then focus purely on assembling commands.

PowerShellRunResult result = PowerShellRunner.Run(ps => ps
    .AddCommand("Get-Service")
    .AddCommand("Where-Object")
        .AddParameter("Property", "Status")
        .AddParameter("EQ", "Running")
    .AddCommand("Select-Object")
        .AddParameter("First", 10)
        .AddParameter("Property", new[] { "Name", "DisplayName", "Status" }));

foreach (PSObject row in result.Output)
{
    Console.WriteLine($"{row.Properties["Name"]?.Value}: {row.Properties["Status"]?.Value}");
}

foreach (ErrorRecord error in result.Errors)
{
    Console.Error.WriteLine(error.Exception.Message);
}

That said, assembling PowerShell-specific condition syntax from C# — like the Where-Object in this example — can get a little hard to read. Simple commands and parameters are fine with AddCommand / AddParameter, but for complex filters and aggregations, a fixed PowerShell script is sometimes more readable. Even then, the policy of never concatenating external input directly into the script string does not change.

12. Shape Complex Output into Objects on the PowerShell Side

When combining C# and PowerShell, design becomes easier if you decide which side owns what.

We recommend this division:

Owner Responsibilities
PowerShell Operations close to Windows and its modules, existing scripts, running admin commands
C# UI, input validation, type conversion, business logic, persistence, API integration

On the PowerShell side, shape the final output into a [pscustomobject].

Get-Service |
  Where-Object Status -eq 'Running' |
  Select-Object Name, DisplayName, Status

Or build a [pscustomobject] explicitly:

$services = Get-Service | Where-Object Status -eq 'Running'

[pscustomobject]@{
    Count = $services.Count
    Names = $services.Name
}

On the C# side, read the properties of the returned PSObject and convert them into your application’s own types.

With this shape, PowerShell implementation details do not leak too far into the C# side.

13. Practical Caveats

When running PowerShell from C#, code that runs is not enough. In practice, checking these points early keeps you safe.

The executing user’s privileges

PowerShell runs with the privileges of the user running the C# application. Commands that require administrator privileges will fail when run as a normal user. Service operations, event logs, certificates, the registry, Hyper-V, and Microsoft 365 administration modules all require thinking through the privilege boundaries.

32-bit / 64-bit differences

On Windows, what a 32-bit process and a 64-bit process can see in the registry and in available modules can differ. If you are building a Windows administration tool, assuming x64 execution as the baseline will reduce trouble.

Whether the modules exist in the runtime environment

Adding the PowerShell SDK to a C# application does not automatically bring along every PowerShell module. For example, if you use a product-specific management module or an internal module, you must confirm that the module exists in the runtime environment and which path it will be loaded from.

In GUI apps, don’t block the UI thread

When running PowerShell from WinForms or WPF, executing heavy work directly on the UI thread freezes the window. In that case, run it as background work and update the UI on completion. The PowerShell SDK also has asynchronous execution APIs, but it starts with one rule: never run a long Invoke() on the UI thread.

Distribution size

Microsoft.PowerShell.SDK is convenient, but it adds dependencies to your application. Acceptable for a small utility, but depending on your distribution format and update mechanism, the size can become a concern. Verify early with your actual distribution method — ClickOnce, MSIX, single-file exe, or internal deployment tooling.

14. The Benefits of Receiving Objects Instead of Strings

Finally, why insist so much on PSObject? Running PowerShell as an external process and reading standard output is easy:

PowerShell output
  ↓
String
  ↓
Split / regex / Substring
  ↓
C# values

But this method depends on the display format. It breaks easily depending on column widths, locale, line endings, whitespace, error messages, and delimiters appearing inside values.

With the PowerShell SDK, the flow becomes:

PowerShell output
  ↓
PSObject
  ↓
Properties / BaseObject
  ↓
C# types

Here you extract values based on the data structure, not the display format. For business applications and admin tools, the latter is far easier to maintain.

15. Conclusion

If you want to run PowerShell from C# and work with the results, it is worth considering the PowerShell SDK rather than just launching powershell.exe and reading standard output.

The basic flow:

Add Microsoft.PowerShell.SDK
  ↓
Create the execution object with PowerShell.Create()
  ↓
Assemble the work with AddCommand / AddParameter / AddScript
  ↓
Run with Invoke()
  ↓
Receive a Collection<PSObject>
  ↓
Extract values via BaseObject or Properties
  ↓
Convert to C# DTOs / records / classes

The three most important points in practice:

  • For downstream processing in C#, use Select-Object or [pscustomobject], not Format-Table
  • Never embed user input directly into an AddScript string; pass it via AddParameter wherever possible
  • Handle PSObject at the boundary, and convert to C# types inside the application

PowerShell is strong at Windows administration and reusing existing assets; C# is strong at building applications, UIs, and type-safe business logic. Connect the two well, and you can gradually build your existing PowerShell scripts into a .NET application without throwing them away.

References

  • The complete sample code for this article (library, demo, unit tests) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/csharp-run-powershell-receive-objects
  • Microsoft Learn: Windows PowerShell Host Quickstart
    https://learn.microsoft.com/en-us/powershell/scripting/developer/hosting/windows-powershell-host-quickstart
  • Microsoft Learn: Adding and invoking commands
    https://learn.microsoft.com/en-us/powershell/scripting/developer/hosting/adding-and-invoking-commands
  • Microsoft Learn: PowerShell Class
    https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.powershell
  • Microsoft Learn: PSObject Class
    https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.psobject
  • NuGet Gallery: Microsoft.PowerShell.SDK
    https://www.nuget.org/packages/Microsoft.PowerShell.SDK/
  • NuGet Gallery: Microsoft.PowerShell.5.1.ReferenceAssemblies
    https://www.nuget.org/packages/Microsoft.PowerShell.5.1.ReferenceAssemblies/

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog