C#(CSharp)でPowerShellを実行して、オブジェクトとして受け取る方法

· · C#, CSharp, PowerShell, Windows, .NET, 自動化, 既存資産活用

C# から PowerShell を実行したい場面は、業務アプリや社内ツールではよくあります。たとえば、次のような処理です。

  • Windows のサービス一覧を取得する
  • プロセスやイベントログを調べる
  • 既存の PowerShell スクリプトを C# アプリから呼び出す
  • 管理者向けの小さな GUI ツールから PowerShell コマンドを実行する
  • PowerShell 側にある既存の自動化資産を、.NET アプリに少しずつ取り込む

単純に実行するだけなら、powershell.exepwsh.exe を外部プロセスとして起動し、標準出力を文字列として読む方法でも動きます。ただ、その方法では PowerShell の良さである「オブジェクトのパイプライン」が失われます。

PowerShell の結果は、本来はただのテキストではありません。Get-Process の結果はプロセスのオブジェクトであり、Get-Service の結果はサービスのオブジェクトです。C# 側でもその構造を保ったまま受け取れると、文字列のパースが不要になり、処理がかなり安全になります。

この記事では、C# から PowerShell を実行し、結果を PSObject として受け取る基本を整理します。

なお、この記事に登場するコードは、ビルド・実行できるサンプル一式(実行ラッパーと変換処理のライブラリ、記事の各章を実演するコンソールデモ、PSObject の受け取りとエラー処理を検証するユニットテスト)として GitHub で公開しています。

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

1. 外部プロセス起動ではなく、PowerShell SDK を使う

C# から PowerShell を呼び出す方法は、大きく分けると2つあります。

方法 特徴 向いている場面
ProcessStartInfopowershell.exe / pwsh.exe を起動する 標準出力・標準エラーを文字列として読む 既存バッチの単純実行、ログを残すだけの処理
System.Management.Automation.PowerShell を使う 結果を PSObject として受け取れる C# 側で結果を加工する処理、管理ツール、業務アプリ

この記事で扱うのは後者です。System.Management.Automation.PowerShell を使うと、PowerShell のパイプラインを C# のコードから組み立てて実行できます。重要なのは、戻り値が文字列ではなく、基本的には Collection<PSObject> になることです。

つまり、次のような考え方になります。

PowerShell コマンドを実行する
  ↓
結果を PSObject のコレクションとして受け取る
  ↓
BaseObject や Properties から値を取り出す
  ↓
必要なら C# の DTO / record / class に変換する

PowerShell の出力を文字列で分解するのではなく、最初からオブジェクトとして扱うのがポイントです。

2. 前提環境

この記事では、.NET 8 のコンソールアプリを例にします。PowerShell SDK はバージョンによって対象の .NET が違うため、プロジェクトのターゲットフレームワークに合わせて選びます。

2026年6月時点では、たとえば次のように考えると分かりやすいです。

C# アプリのターゲット 例として使う PowerShell SDK 備考
.NET 8 Microsoft.PowerShell.SDK 7.4 系 .NET 8 アプリで使いやすい
.NET 10 Microsoft.PowerShell.SDK 7.6 系 新しい PowerShell SDK を使う場合の候補
.NET Framework Microsoft.PowerShell.5.1.ReferenceAssemblies Windows PowerShell 5.1 向け。新規開発では要件を確認する

ここでは .NET 8 の例として、Microsoft.PowerShell.SDK 7.4.16 を使います。

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

.csproj は、たとえば次のようになります。

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

バージョンは固定しておくのがおすすめです。PowerShell SDK は便利ですが、アプリの実行環境、対象 .NET、PowerShell モジュールの互換性の影響を受けます。業務アプリでは「開発環境で動いた最新バージョン」をそのまま使うよりも、動作確認したバージョンを明示しておく方が安全です。

3. 最小コード:PowerShell を実行して PSObject を受け取る

まずは、現在の C# アプリ自身のプロセスを 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");
    }
}

ここで押さえたい点は3つあります。PowerShell.Create() で PowerShell の実行オブジェクトを作ること、AddCommand("Get-Process")AddParameter("Id", currentProcessId) でコマンドとパラメーターを組み立てていること、そして Invoke() の戻り値が Collection<PSObject> であることです。

PSObject は、PowerShell が出力する値を包むラッパーです。中にある元の .NET オブジェクトを見たい場合は、BaseObject を見ます。この例では、Get-Process の結果の中身が System.Diagnostics.Process として取り出せます。

4. BaseObject と Properties の使い分け

C# で PowerShell の結果を扱うとき、最初に迷うのが次の2つです。

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

どちらを使うかの目安は次の通りです。

取り出し方 使う場面
BaseObject PowerShell が返した元の .NET オブジェクトをそのまま使いたい場合
Properties["..."] Select-Object[pscustomobject] で作った列を取り出したい場合

Get-Process のようなコマンドをそのまま実行した場合、BaseObject には元の .NET オブジェクトが入っていることがあります。一方で、PowerShell 側で Select-Object を使って列を整形した場合、結果は PowerShell のカスタムオブジェクトとして返ってくることが多くなります。その場合は、Properties から列名で値を取る方が自然です。

5. Select-Object した結果を C# で読む

実務では、PowerShell から返ってくる全プロパティが必要なことはあまりありません。必要な列だけ C# 側に渡したい場合は、PowerShell のパイプラインで Select-Object を使います。

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}");
}

このコードは、PowerShell では次のようなパイプラインに相当します。

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

C# から見ると、AddCommand を続けて呼ぶことで、PowerShell のパイプラインを作っています。

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

このように書くと、前のコマンドの出力が次のコマンドへ渡されます。

Select-Object で列を絞った後は、row.Properties["Name"]?.Value のように列名で値を取り出します。

6. C# の record に変換する

PSObject のままアプリ全体に渡すと、後続のコードが PowerShell に依存しすぎます。画面表示や業務処理に使うなら、C# 側の型に変換した方が扱いやすくなります。

たとえば、プロセス情報を次の record に変換します。

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

変換処理は次のように分けておくと、見通しが良くなります。

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);
}

使う側はこうなります。

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

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

PSObject は PowerShell との境界で扱い、アプリ内部では ProcessSummary のような通常の C# の型に変換する。

この分離をしておくと、後で PowerShell コマンドを変更しても、影響範囲を小さくできます。

7. PSCustomObject を返すと C# 側で扱いやすい

PowerShell 側で複数の値をまとめて返したい場合は、[pscustomobject] を使うと便利です。

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}");
}

PowerShell スクリプトの最後で [pscustomobject] を返すと、C# 側では Properties から名前で値を取り出せます。複雑な文字列を返して C# 側で分割するよりも、かなり安全です。

避けたい例は、次のような出力です。

"$MachineName,$PowerShellVersion,$CurrentDirectory"

この方法は一見簡単ですが、値の中にカンマや改行が入ると壊れます。

PowerShell 側ではオブジェクトを返し、C# 側ではプロパティとして読む。この形にしておくと、後から列が増えても対応しやすくなります。

8. AddScript にユーザー入力を直接埋め込まない

PowerShell SDK を使う場合でも、文字列としてスクリプトを組み立てると危険です。たとえば、次のようなコードは避けた方がよいです。

// 避ける例
string userInputPath = GetPathFromUser();
string script = $"Get-ChildItem -Path '{userInputPath}'";

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

この書き方では、ユーザー入力が PowerShell のコードとして解釈される余地があります。PowerShell コマンドに値を渡す場合は、できるだけ AddCommandAddParameter を使います。

string userInputPath = GetPathFromUser();

using PowerShell ps = PowerShell.Create();

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

AddParameter で渡した値は、PowerShell のコード文字列として連結されるのではなく、パラメーター値として扱われます。

実務では、次のように使い分けるのが無難です。

書き方 使いどころ
AddCommand / AddParameter C# 側から安全にコマンドを組み立てたい場合
AddScript 固定の短いスクリプトを実行する場合、既存スクリプトを読み込む場合
文字列連結した AddScript 原則避ける。使うなら入力値の検証とエスケープを慎重に行う

PowerShell を C# に組み込むと、アプリの機能として強い操作ができるようになります。便利な反面、ユーザー入力をそのままスクリプト化しない、という一線だけは守る必要があります。

9. Format-Table は最後の画面表示用。C# に渡す前には使わない

PowerShell の結果を C# でオブジェクトとして受け取りたい場合、Format-TableFormat-List は基本的に使いません。

たとえば、次のような PowerShell は人間が画面で見るには便利です。

Get-Service | Format-Table Name, Status

しかし、C# 側で受け取る前に Format-Table を使うと、結果はサービスのオブジェクトではなく、表示用のフォーマット情報になってしまいます。C# で扱いたい場合は、Select-Object を使います。

Get-Service | Select-Object Name, Status

C# から書くなら、次のようになります。

using PowerShell ps = PowerShell.Create();

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

考え方はシンプルです。

画面で見やすくするだけ → Format-Table / Format-List
C# で後続処理に使う    → Select-Object / PSCustomObject

これは PowerShell 単体で使うときにも言えることですが、C# と連携する場合は特に効いてきます。

10. エラーを受け取る

PowerShell では、出力とエラーは別のストリームです。Invoke() の戻り値だけを見ていると、エラーを見落とすことがあります。基本形は次の通りです。

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 のコマンドレットには、処理を止めるエラーと、処理を続けるエラーがあります。C# 側で例外として扱いたい場合は、ErrorActionStop を指定する方法があります。

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}");
}

どちらが良いかは、アプリの性質によります。管理ツールで「一部失敗しても一覧は出したい」場合は、エラーストリームを回収して画面に表示する方が向いていますし、「失敗したら処理全体を止めたい」場合は、ErrorAction Stop で例外として扱う方が分かりやすくなります。

11. 小さな実行ラッパーを作る

何度も PowerShell を呼び出すアプリでは、毎回同じエラー処理を書くと散らかります。簡単なラッパーを用意しておくと便利です。

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());
    }
}

使う側は、コマンドの組み立てだけに集中できます。

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);
}

ただし、この例の Where-Object のように、PowerShell 特有の条件指定を C# から組み立てると少し読みにくくなることがあります。単純なコマンドとパラメーターは AddCommand / AddParameter でよいですが、複雑なフィルターや集計は、固定の PowerShell スクリプトとして用意した方が読みやすい場合もあります。その場合でも、外部入力をスクリプト文字列へ直接連結しない方針は変わりません。

12. 複雑な処理は PowerShell 側でオブジェクトに整える

C# と PowerShell を組み合わせるときは、どちらで何を担当するかを分けると設計しやすくなります。

おすすめは次の分担です。

担当 やること
PowerShell Windows やモジュールに近い操作、既存スクリプト、管理コマンドの実行
C# UI、入力検証、型変換、業務ロジック、保存、API 連携

PowerShell 側では、最終的な出力を [pscustomobject] に整えます。

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

または、明示的に [pscustomobject] を作ります。

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

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

C# 側では、返ってきた PSObject のプロパティを読んで、自分のアプリの型に変換します。

この形にしておくと、PowerShell の細かい実装を C# 側に漏らしすぎずに済みます。

13. 実務でよくある注意点

C# から PowerShell を実行する場合、コードが動くだけでは不十分です。実務では、次の点を早めに確認しておくと安全です。

実行ユーザーの権限

PowerShell は、C# アプリを実行しているユーザーの権限で動きます。管理者権限が必要なコマンドは、通常ユーザーで実行しても失敗します。サービス操作、イベントログ、証明書、レジストリ、Hyper-V、Microsoft 365 管理系のモジュールなどでは、権限の切り分けが必要になります。

32bit / 64bit の違い

Windows では、32bit プロセスと 64bit プロセスで見えるレジストリやモジュールが変わることがあります。Windows 管理用ツールとして作るなら、基本的には x64 で動かす前提にした方がトラブルを減らせます。

実行環境にモジュールがあるか

C# アプリに PowerShell SDK を入れても、すべての PowerShell モジュールが自動的に入るわけではありません。たとえば、特定製品の管理モジュールや社内モジュールを使う場合は、実行環境にそのモジュールが存在するか、どのパスから読み込むかを確認する必要があります。

画面アプリでは UI スレッドを止めない

WinForms や WPF から PowerShell を実行する場合、重い処理を UI スレッドで直接実行すると画面が固まります。その場合は、バックグラウンド処理として実行し、完了後に UI を更新する設計にします。PowerShell SDK には非同期実行の API もありますが、まずは「UI スレッドで長時間の Invoke() をしない」という方針を守るところからです。

アプリ配布時のサイズ

Microsoft.PowerShell.SDK は便利ですが、アプリに含まれる依存関係も増えます。小さなユーティリティでは許容できても、配布形式や更新方式によってはサイズが気になることがあります。ClickOnce、MSIX、単体 exe 配布、社内配布ツールなど、実際の配布方式で早めに検証しておくと安心です。

14. 文字列ではなくオブジェクトで受け取る利点

最後に、なぜここまで PSObject にこだわるのか。外部プロセス起動で PowerShell を実行し、標準出力を読む方法は簡単です。

PowerShell の出力
  ↓
文字列
  ↓
Split / 正規表現 / Substring
  ↓
C# の値

ただし、この方法は表示形式に依存します。列幅、ロケール、改行、空白、エラーメッセージ、値の中の区切り文字によって壊れやすくなります。

一方で、PowerShell SDK を使うと次の流れになります。

PowerShell の出力
  ↓
PSObject
  ↓
Properties / BaseObject
  ↓
C# の型

こちらは、表示形式ではなくデータ構造に基づいて値を取り出せます。業務アプリや管理ツールでは、後者の方が保守しやすくなります。

15. まとめ

C# から PowerShell を実行して結果を扱うなら、単に powershell.exe を起動して標準出力を読むだけでなく、PowerShell SDK を使う方法を検討する価値があります。

基本の流れは次の通りです。

Microsoft.PowerShell.SDK を追加する
  ↓
PowerShell.Create() で実行オブジェクトを作る
  ↓
AddCommand / AddParameter / AddScript で処理を組み立てる
  ↓
Invoke() で実行する
  ↓
Collection<PSObject> として受け取る
  ↓
BaseObject または Properties から値を取り出す
  ↓
C# の DTO / record / class に変換する

実務で特に重要なのは、次の3点です。

  • C# で後続処理に使うなら、Format-Table ではなく Select-Object[pscustomobject] を使う
  • ユーザー入力を AddScript の文字列へ直接埋め込まず、できるだけ AddParameter で渡す
  • PSObject は境界で扱い、アプリ内部では C# の型に変換する

PowerShell は Windows 管理や既存資産活用に強く、C# はアプリ化、画面化、型安全な業務処理に強いです。両方をうまくつなぐと、既存の PowerShell スクリプトを捨てずに、少しずつ .NET アプリとして整備できます。

参考情報

  • この記事のサンプルコード一式(ライブラリ、デモ、ユニットテスト) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/csharp-run-powershell-receive-objects
  • Microsoft Learn: Windows PowerShell ホストのクイック スタート
    https://learn.microsoft.com/ja-jp/powershell/scripting/developer/hosting/windows-powershell-host-quickstart
  • Microsoft Learn: コマンドを追加し、呼び出す
    https://learn.microsoft.com/ja-jp/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/ja-jp/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/

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る