PowerShell aus C# (CSharp) ausführen und die Ergebnisse als Objekte empfangen
· Go Komura · C#, CSharp, PowerShell, Windows, .NET, Automatisierung, Nutzung vorhandener Ressourcen
Situationen, in denen Sie PowerShell aus C# ausführen möchten, kommen in Geschäftsanwendungen und internen Werkzeugen ständig vor. Zum Beispiel bei folgenden Aufgaben:
- Die Liste der Windows-Dienste abrufen
- Prozesse oder Ereignisprotokolle untersuchen
- Ein bestehendes PowerShell-Skript aus einer C#-Anwendung heraus aufrufen
- PowerShell-Befehle aus einem kleinen GUI-Werkzeug für Administratoren ausführen
- Bestehende Automatisierungsbestände auf der PowerShell-Seite Stück für Stück in eine .NET-Anwendung überführen
Wenn Sie es nur einfach ausführen möchten, funktioniert es auch, powershell.exe oder pwsh.exe als externen Prozess zu starten und die Standardausgabe als Zeichenkette zu lesen. Bei diesem Ansatz geht jedoch genau das verloren, was PowerShell auszeichnet: die Objekt-Pipeline.
Das Ergebnis von PowerShell ist eigentlich kein reiner Text. Das Ergebnis von Get-Process sind Prozessobjekte, das Ergebnis von Get-Service sind Dienstobjekte. Kann die C#-Seite diese Struktur erhalten empfangen, entfällt das Parsen von Zeichenketten, und die Verarbeitung wird deutlich sicherer.
Dieser Artikel ordnet die Grundlagen, wie man PowerShell aus C# ausführt und das Ergebnis als PSObject empfängt.
Der gesamte in diesem Artikel gezeigte Code ist außerdem als bau- und lauffähiges Beispielpaket (eine Bibliothek mit Ausführungs-Wrapper und Umwandlungslogik, eine Konsolen-Demo, die jedes Kapitel des Artikels vorführt, sowie Unit-Tests, die den Empfang von PSObject und die Fehlerbehandlung prüfen) auf GitHub veröffentlicht.
csharp-run-powershell-receive-objects - komurasoft-blog-samples (GitHub)
1. Den PowerShell SDK statt eines externen Prozessstarts verwenden
Es gibt grob zwei Möglichkeiten, PowerShell aus C# aufzurufen.
| Ansatz | Merkmale | Geeignet für |
|---|---|---|
powershell.exe / pwsh.exe über ProcessStartInfo starten |
Standardausgabe und Standardfehler als Zeichenkette lesen | Einfache Ausführung bestehender Batches, Vorgänge, die nur Protokolle hinterlassen sollen |
System.Management.Automation.PowerShell verwenden |
Ergebnis als PSObject empfangen |
Verarbeitung des Ergebnisses auf der C#-Seite, Verwaltungswerkzeuge, Geschäftsanwendungen |
In diesem Artikel geht es um den zweiten Ansatz. Mit System.Management.Automation.PowerShell lässt sich eine PowerShell-Pipeline aus C#-Code zusammensetzen und ausführen. Wichtig ist, dass der Rückgabewert keine Zeichenkette ist, sondern grundsätzlich ein Collection<PSObject>.
Das führt zu folgendem Denkmodell:
Einen PowerShell-Befehl ausführen
↓
Das Ergebnis als Sammlung von PSObject empfangen
↓
Werte über BaseObject oder Properties auslesen
↓
Bei Bedarf in ein C#-DTO / record / class umwandeln
Der Kernpunkt ist, die PowerShell-Ausgabe von Anfang an als Objekt zu behandeln, statt sie als Zeichenkette zu zerlegen.
2. Voraussetzungen der Umgebung
In diesem Artikel dient eine .NET-8-Konsolenanwendung als Beispiel. Da das PowerShell SDK je nach Version unterschiedliche .NET-Zielversionen unterstützt, wählen Sie es passend zum Zielframework Ihres Projekts.
Stand Juni 2026 ist folgende Faustregel hilfreich:
| Ziel der C#-Anwendung | Beispielhaft verwendetes PowerShell SDK | Anmerkung |
|---|---|---|
| .NET 8 | Microsoft.PowerShell.SDK 7.4er-Reihe |
Gut geeignet für .NET-8-Anwendungen |
| .NET 10 | Microsoft.PowerShell.SDK 7.6er-Reihe |
Kandidat, wenn ein neueres PowerShell SDK verwendet werden soll |
| .NET Framework | Microsoft.PowerShell.5.1.ReferenceAssemblies |
Für Windows PowerShell 5.1. Bei Neuentwicklungen die Anforderungen prüfen |
Als Beispiel für .NET 8 verwenden wir hier Microsoft.PowerShell.SDK 7.4.16.
dotnet new console -n PowerShellObjectSample
cd PowerShellObjectSample
dotnet add package Microsoft.PowerShell.SDK --version 7.4.16
Die .csproj sieht dann zum Beispiel so aus:
<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>
Es empfiehlt sich, die Version festzunageln. Das PowerShell SDK ist praktisch, wird aber von der Laufzeitumgebung der Anwendung, der Ziel-.NET-Version und der Kompatibilität der PowerShell-Module beeinflusst. Bei Geschäftsanwendungen ist es sicherer, eine geprüfte Version explizit festzulegen, statt einfach „die neueste Version, die in der Entwicklungsumgebung funktioniert hat“ zu übernehmen.
Was sich bei der Entwicklung für .NET Framework unterscheidet
Es gibt auch Fälle, in denen das zu wartende System eine Windows-Forms- oder WPF-Anwendung auf .NET Framework ist und Sie von dort PowerShell aufrufen möchten. Die Codebeispiele in diesem Artikel setzen das aktuelle .NET voraus, aber die Schreibweise auf der C#-Seite ist fast identisch. Die Unterschiede liegen in folgenden Punkten:
| Aspekt | Aktuelles .NET + Microsoft.PowerShell.SDK |
.NET Framework + Microsoft.PowerShell.5.1.ReferenceAssemblies |
|---|---|---|
| Ausgeführtes PowerShell | PowerShell-7er-Reihe, im Paket enthalten | In Windows mitgelieferte Windows PowerShell 5.1 |
| Rolle des NuGet-Pakets | Enthält die eigentliche Implementierung | Nur Referenzassemblys. Die Laufzeitassembly stammt vom Betriebssystem |
| Verfügbare Syntax und Cmdlets | Umfang der PowerShell-7er-Reihe | Umfang von 5.1. ForEach-Object -Parallel, ??, der ternäre Operator und Ähnliches sind nicht verfügbar |
| Suchpfad für Module | $env:PSModulePath von PowerShell 7 |
$env:PSModulePath von Windows PowerShell |
| Asynchrone Ausführung | InvokeAsync steht zur Verfügung |
BeginInvoke / EndInvoke, oder Invoke() mit Task.Run umschließen |
| Schreibweise auf der C#-Seite | PowerShell.Create(), AddCommand, AddParameter, Invoke(), Collection<PSObject> |
Gleich |
| Verteilungsgröße | Groß, da das gesamte SDK mitgeliefert wird | Klein, da das Betriebssystem verwendet wird |
Die .csproj sieht dann zum Beispiel so aus:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net48</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.PowerShell.5.1.ReferenceAssemblies" Version="1.0.0" />
</ItemGroup>
</Project>
In der Praxis wirkt sich am stärksten aus, dass tatsächlich Windows PowerShell 5.1 ausgeführt wird. Übernehmen Sie ein bestehendes PowerShell-Skript, das für PowerShell 7 geschrieben wurde, kann es unter 5.1 zu Syntaxfehlern kommen. Umgekehrt entscheiden sich manche gerade deshalb für diesen Weg, weil sie ein altes Modul verwenden möchten, das nur unter Windows PowerShell 5.1 läuft.
Der Code in den Kapiteln 3 bis 12 dieses Artikels verwendet kein InvokeAsync und funktioniert daher unverändert auch auf der .NET-Framework-Seite. Nur das Thema Asynchronität wird gesondert in Kapitel 13 behandelt.
3. Minimalcode: PowerShell ausführen und PSObject empfangen
Zunächst rufen wir mit PowerShell den Prozess der aktuell laufenden C#-Anwendung selbst ab.
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");
}
}
Drei Dinge sollten Sie hier festhalten: Mit PowerShell.Create() wird das PowerShell-Ausführungsobjekt erzeugt, mit AddCommand("Get-Process") und AddParameter("Id", currentProcessId) werden Befehl und Parameter zusammengesetzt, und der Rückgabewert von Invoke() ist ein Collection<PSObject>.
PSObject ist eine Wrapper-Klasse, die den von PowerShell ausgegebenen Wert umschließt. Möchten Sie das darin enthaltene ursprüngliche .NET-Objekt sehen, schauen Sie sich BaseObject an. In diesem Beispiel lässt sich der Inhalt des Get-Process-Ergebnisses als System.Diagnostics.Process abrufen.
4. BaseObject und Properties richtig einsetzen
Wenn Sie PowerShell-Ergebnisse in C# verarbeiten, sind das die beiden Dinge, bei denen Sie zuerst ins Grübeln kommen:
item.BaseObject
item.Properties["Name"]?.Value
Als Richtschnur, welche Sie verwenden sollten, gilt Folgendes:
| Zugriffsweise | Einsatzbereich |
|---|---|
BaseObject |
Wenn Sie das von PowerShell zurückgegebene ursprüngliche .NET-Objekt unverändert verwenden möchten |
Properties["..."] |
Wenn Sie Spalten auslesen möchten, die mit Select-Object oder [pscustomobject] erzeugt wurden |
Führen Sie einen Befehl wie Get-Process direkt aus, kann BaseObject das ursprüngliche .NET-Objekt enthalten. Formt die PowerShell-Seite die Spalten dagegen mit Select-Object, kommt das Ergebnis meist als PowerShell-Custom-Objekt zurück. In diesem Fall ist es natürlicher, den Wert über den Spaltennamen aus Properties zu holen.
5. Das Ergebnis von Select-Object in C# lesen
In der Praxis benötigen Sie selten alle Eigenschaften, die PowerShell zurückgibt. Möchten Sie nur die benötigten Spalten an die C#-Seite übergeben, verwenden Sie Select-Object in der 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}");
}
Dieser Code entspricht in PowerShell folgender Pipeline:
Get-Process |
Sort-Object -Property CPU -Descending |
Select-Object -First 10 -Property Name, Id, CPU, WorkingSet
Aus C#-Sicht wird die PowerShell-Pipeline gebaut, indem AddCommand mehrfach hintereinander aufgerufen wird.
.AddCommand("Get-Process")
.AddCommand("Sort-Object")
.AddCommand("Select-Object")
So geschrieben wird die Ausgabe des vorherigen Befehls an den nächsten Befehl weitergegeben.
Nachdem die Spalten mit Select-Object eingeschränkt wurden, rufen Sie die Werte wie in row.Properties["Name"]?.Value über den Spaltennamen ab.
6. In einen C#-record umwandeln
Wird PSObject unverändert durch die gesamte Anwendung gereicht, hängt der nachfolgende Code zu stark von PowerShell ab. Für die Bildschirmanzeige oder die Geschäftslogik lässt sich leichter arbeiten, wenn Sie zuvor in einen C#-seitigen Typ umwandeln.
Zum Beispiel wandeln wir Prozessinformationen in folgenden record um:
public sealed record ProcessSummary(
string Name,
int Id,
double? Cpu,
long WorkingSet);
Trennt man die Umwandlungslogik wie folgt ab, bleibt der Code übersichtlich.
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);
}
Auf der Verwendungsseite sieht es dann so aus:
List<ProcessSummary> processes = rows
.Select(ToProcessSummary)
.ToList();
foreach (ProcessSummary process in processes)
{
Console.WriteLine($"{process.Id}: {process.Name}");
}
PSObject wird an der Grenze zu PowerShell behandelt, im Inneren der Anwendung wird in einen gewöhnlichen C#-Typ wie ProcessSummary umgewandelt.
Mit dieser Trennung bleibt der Auswirkungsbereich klein, selbst wenn Sie den PowerShell-Befehl später ändern.
7. Ein PSCustomObject zurückzugeben erleichtert die C#-Seite
Möchten Sie auf der PowerShell-Seite mehrere Werte gebündelt zurückgeben, ist [pscustomobject] praktisch.
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}");
}
Gibt ein PowerShell-Skript am Ende ein [pscustomobject] zurück, kann die C#-Seite die Werte über Properties anhand des Namens abrufen. Das ist deutlich sicherer, als eine komplexe Zeichenkette zurückzugeben und sie auf der C#-Seite aufzuteilen.
Ein Beispiel, das Sie vermeiden sollten, ist eine Ausgabe wie diese:
"$MachineName,$PowerShellVersion,$CurrentDirectory"
Diese Methode wirkt auf den ersten Blick einfach, bricht aber, sobald ein Wert ein Komma oder einen Zeilenumbruch enthält.
Auf der PowerShell-Seite werden Objekte zurückgegeben, auf der C#-Seite werden sie als Eigenschaften gelesen. Mit dieser Form lässt sich später leicht reagieren, wenn weitere Spalten hinzukommen.
8. Benutzereingaben nicht direkt in AddScript einbetten
Auch bei Verwendung des PowerShell SDK ist es gefährlich, das Skript als Zeichenkette zusammenzusetzen. Code wie der folgende sollte zum Beispiel vermieden werden:
// Zu vermeidendes Beispiel
string userInputPath = GetPathFromUser();
string script = $"Get-ChildItem -Path '{userInputPath}'";
using PowerShell ps = PowerShell.Create();
ps.AddScript(script).Invoke();
Bei dieser Schreibweise kann die Benutzereingabe als PowerShell-Code interpretiert werden. Um Werte an PowerShell-Befehle zu übergeben, verwenden Sie möglichst AddCommand und AddParameter.
string userInputPath = GetPathFromUser();
using PowerShell ps = PowerShell.Create();
Collection<PSObject> files = ps
.AddCommand("Get-ChildItem")
.AddParameter("Path", userInputPath)
.AddParameter("File", true)
.Invoke();
Ein über AddParameter übergebener Wert wird nicht in die PowerShell-Code-Zeichenkette verkettet, sondern als Parameterwert behandelt.
In der Praxis ist folgende Aufteilung ratsam:
| Schreibweise | Einsatzbereich |
|---|---|
AddCommand / AddParameter |
Wenn Sie von der C#-Seite aus sicher einen Befehl zusammensetzen möchten |
AddScript |
Zum Ausführen fester kurzer Skripte oder zum Laden bestehender Skripte |
AddScript mit Zeichenkettenverkettung |
Grundsätzlich vermeiden. Falls doch verwendet, Eingabewerte sorgfältig validieren und escapen |
Bindet man PowerShell in C# ein, erhält die Anwendung als Funktion sehr weitreichende Möglichkeiten. Bei aller Bequemlichkeit muss dabei aber eine Grenze eingehalten werden: Benutzereingaben dürfen nicht unverändert zu Skript werden.
9. Format-Table ist nur für die abschließende Bildschirmanzeige – nicht vor der Übergabe an C# verwenden
Möchten Sie PowerShell-Ergebnisse in C# als Objekte empfangen, verwenden Sie grundsätzlich kein Format-Table oder Format-List.
Zum Beispiel ist folgendes PowerShell praktisch, wenn ein Mensch es auf dem Bildschirm betrachtet:
Get-Service | Format-Table Name, Status
Verwenden Sie jedoch Format-Table, bevor die C#-Seite das Ergebnis empfängt, wird daraus nicht mehr das Service-Objekt, sondern Formatierungsinformationen für die Anzeige. Möchten Sie das Ergebnis in C# verarbeiten, verwenden Sie Select-Object.
Get-Service | Select-Object Name, Status
Aus C# geschrieben sieht das so aus:
using PowerShell ps = PowerShell.Create();
Collection<PSObject> services = ps
.AddCommand("Get-Service")
.AddCommand("Select-Object")
.AddParameter("Property", new[] { "Name", "Status" })
.Invoke();
Das Prinzip ist einfach:
Nur für eine übersichtliche Bildschirmanzeige → Format-Table / Format-List
Für die Weiterverarbeitung in C# → Select-Object / PSCustomObject
Das gilt schon bei der alleinigen Verwendung von PowerShell, wirkt sich aber besonders stark aus, wenn Sie mit C# zusammenarbeiten.
10. Fehler empfangen
In PowerShell sind Ausgabe und Fehler getrennte Streams. Wenn Sie nur auf den Rückgabewert von Invoke() schauen, können Sie Fehler übersehen. Die Grundform sieht so aus:
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 kennen abbrechende Fehler und Fehler, bei denen die Verarbeitung fortgesetzt wird. Möchten Sie sie auf der C#-Seite als Exception behandeln, können Sie ErrorAction auf Stop setzen.
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}");
}
Was besser ist, hängt von der Art der Anwendung ab. Möchten Sie bei einem Verwaltungswerkzeug „auch bei Teilfehlern noch die Liste anzeigen“, eignet sich das Sammeln des Fehlerstreams und die Anzeige auf dem Bildschirm besser. Möchten Sie „bei einem Fehler die gesamte Verarbeitung stoppen“, ist die Behandlung als Exception über ErrorAction Stop klarer.
11. Einen kleinen Ausführungs-Wrapper bauen
In einer Anwendung, die PowerShell wiederholt aufruft, wird der Code unübersichtlich, wenn Sie jedes Mal dieselbe Fehlerbehandlung schreiben. Ein einfacher Wrapper ist hier praktisch.
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());
}
}
Die aufrufende Seite kann sich dann ausschließlich auf das Zusammensetzen des Befehls konzentrieren.
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);
}
Allerdings kann es, wie beim Where-Object in diesem Beispiel, etwas schwer lesbar werden, wenn Sie PowerShell-spezifische Bedingungsangaben aus C# heraus zusammensetzen. Für einfache Befehle und Parameter reicht AddCommand / AddParameter, aber für komplexe Filter oder Aggregationen ist es manchmal lesbarer, ein festes PowerShell-Skript vorzubereiten. Auch in diesem Fall bleibt der Grundsatz bestehen, externe Eingaben nicht direkt in die Skript-Zeichenkette zu verketten.
12. Komplexe Verarbeitung auf der PowerShell-Seite zu Objekten formen
Wenn Sie C# und PowerShell kombinieren, lässt sich der Entwurf leichter gestalten, wenn Sie festlegen, wer wofür zuständig ist.
Folgende Aufteilung ist empfehlenswert:
| Zuständigkeit | Aufgabe |
|---|---|
| PowerShell | Operationen nahe an Windows und seinen Modulen, bestehende Skripte, Ausführung von Verwaltungsbefehlen |
| C# | UI, Eingabevalidierung, Typumwandlung, Geschäftslogik, Speicherung, API-Anbindung |
Auf der PowerShell-Seite wird die endgültige Ausgabe zu einem [pscustomobject] geformt.
Get-Service |
Where-Object Status -eq 'Running' |
Select-Object Name, DisplayName, Status
Oder Sie erzeugen explizit ein [pscustomobject]:
$services = Get-Service | Where-Object Status -eq 'Running'
[pscustomobject]@{
Count = $services.Count
Names = $services.Name
}
Auf der C#-Seite werden die Eigenschaften des zurückgegebenen PSObject gelesen und in den Typ der eigenen Anwendung umgewandelt.
Mit dieser Form lässt sich verhindern, dass Implementierungsdetails von PowerShell zu stark auf die C#-Seite durchsickern.
13. Praxisrelevante Hinweise
Wenn Sie PowerShell aus C# ausführen, reicht es nicht, dass der Code einfach läuft. In der Praxis ist es sicherer, folgende Punkte frühzeitig zu prüfen.
Berechtigungen des ausführenden Benutzers
PowerShell läuft mit den Berechtigungen des Benutzers, der die C#-Anwendung ausführt. Befehle, die Administratorrechte benötigen, schlagen fehl, wenn sie als normaler Benutzer ausgeführt werden. Bei Dienstoperationen, Ereignisprotokollen, Zertifikaten, der Registrierung, Hyper-V oder Verwaltungsmodulen für Microsoft 365 ist eine klare Trennung der Berechtigungen erforderlich.
Unterschiede zwischen 32-Bit und 64-Bit
Unter Windows können sich die sichtbare Registrierung und die verfügbaren Module zwischen einem 32-Bit- und einem 64-Bit-Prozess unterscheiden. Bauen Sie ein Windows-Verwaltungswerkzeug, verringert es Probleme, grundsätzlich von einer Ausführung unter x64 auszugehen.
Ob die Module in der Laufzeitumgebung vorhanden sind
Selbst wenn Sie das PowerShell SDK in eine C#-Anwendung einbinden, werden nicht automatisch alle PowerShell-Module mitgeliefert. Verwenden Sie zum Beispiel ein produktspezifisches Verwaltungsmodul oder ein internes Modul, müssen Sie prüfen, ob dieses Modul in der Laufzeitumgebung vorhanden ist und aus welchem Pfad es geladen wird.
In GUI-Anwendungen den UI-Thread nicht blockieren
Führen Sie PowerShell aus WinForms oder WPF aus, friert das Fenster ein, wenn Sie eine aufwendige Operation direkt auf dem UI-Thread ausführen. In diesem Fall gestalten Sie es so, dass die Operation im Hintergrund läuft und die UI erst nach Abschluss aktualisiert wird.
Das PowerShell SDK bietet APIs für die asynchrone Ausführung, deren Verwendung der naheliegendste Weg ist. PowerShell.InvokeAsync gibt ein Task<PSDataCollection<PSObject>> zurück, das sich direkt mit await erwarten lässt.
Zunächst trennen wir den Teil, der PowerShell aufruft, als Methode ab, die die UI nicht berührt.
using System.Management.Automation;
// Die folgenden beiden Methoden sind dafür gedacht, in der Klasse eines
// Fensters oder Formulars zu liegen.
//
// Nimmt unbedingt ein cancellationToken entgegen. Wird die PowerShell-Instanz
// als lokale Variable dieser Methode versteckt gehalten, hat die aufrufende
// Seite keine Möglichkeit, Stop() aufzurufen. Hängt der Befehl oder schließt
// der Benutzer das Fenster, wartet der await-ende Handler weiter, während
// die Schaltfläche deaktiviert bleibt
private static async Task<IReadOnlyList<PSObject>> GetRunningServicesAsync(
CancellationToken cancellationToken)
{
// Kein using. Das Objekt darf erst verworfen werden, nachdem das Stoppen
// abgeschlossen ist (siehe unten)
PowerShell ps = PowerShell.Create();
Task? stopping = null;
try
{
// `-EQ` ist ein Switch ohne Wert; der Vergleichswert wird über `-Value`
// übergeben (die Kurzsyntax lautet `-Property <String> -EQ -Value <Object>`).
// AddParameter("EQ", "Running") scheitert bereits bei der Parameterzuordnung
ps.AddCommand("Get-Service")
.AddCommand("Where-Object")
.AddParameter("Property", "Status")
.AddParameter("EQ")
.AddParameter("Value", "Running")
.AddCommand("Select-Object")
.AddParameter("Property", new[] { "Name", "DisplayName", "Status" });
// Nicht vor dem Start registrieren. Register führt die Callback sofort
// aus, wenn das übergebene Token bereits abgebrochen ist. Das würde
// BeginStop an eine noch nicht gestartete Pipeline schicken, wo es
// wirkungslos verpufft; danach startet InvokeAsync den Befehl, und das
// Stoppsignal ist bereits verbraucht ── mit dem Ergebnis, dass der
// Befehl weiterläuft, obwohl das Fenster geschlossen wurde.
// Erst abfangen, dann starten, dann den Stopp-Zugang anbringen
cancellationToken.ThrowIfCancellationRequested();
Task<PSDataCollection<PSObject>> running = ps.InvokeAsync();
// Bei Abbruch die Pipeline stoppen. Stop() kehrt erst zurück, wenn das
// Stoppen abgeschlossen ist, und der Abbruch kann vom UI-Thread aus
// erfolgen, daher die asynchrone Variante verwenden. Das Stoppen selbst
// wird als Task entgegengenommen; im finally unten wird auf dessen
// Abschluss gewartet
using (cancellationToken.Register(
state => Volatile.Write(ref stopping, StopAsync((PowerShell)state!)), ps))
{
PSDataCollection<PSObject> output = await running;
return output.ToList();
}
}
finally
{
// Wurde ein Stopp angefordert, erst dessen Abschluss abwarten und dann
// verwerfen. Dass await running zurückkehrt und das Stoppen abgeschlossen
// ist, geschieht unabhängig voneinander; würde man hier ohne zu warten
// Dispose aufrufen, griffe das später laufende EndStop auf eine bereits
// verworfene PowerShell-Instanz zu. Und das geschieht in einem
// Threadpool-Callback, wo es keine Stelle gibt, die eine geworfene
// Exception auffängt (der ganze Prozess stürzt ab)
Task? pending = Volatile.Read(ref stopping);
if (pending is not null)
{
try { await pending; }
catch { /* Ein Fehlschlag beim Stoppen soll das eigentliche Ergebnis oder die eigentliche Exception nicht verdecken */ }
}
ps.Dispose();
}
}
// BeginStop / EndStop in einen Task einwickeln. Ruft man EndStop direkt in
// der Callback auf, entkommt dessen Exception dem Threadpool
private static Task StopAsync(PowerShell ps) =>
Task.Factory.FromAsync(ps.BeginStop, ps.EndStop, null);
Die aufrufende Seite ist der Ereignishandler des Fensters oder Formulars. Hier ist async void unbedenklich. Ereignishandler sind einer der wenigen Orte, an denen async void erlaubt ist.
// Für eine WPF-Fensterklasse gedacht.
// Für WinForms ersetzen Sie RoutedEventArgs durch EventArgs,
// IsEnabled durch Enabled und ItemsSource durch DataSource.
// Für das Abbrechen während der Ausführung. Wird sowohl von der
// Abbrechen-Schaltfläche als auch beim Schließen des Fensters verwendet
private CancellationTokenSource? _running;
private async void RunButton_Click(object sender, RoutedEventArgs e)
{
RunButton.IsEnabled = false;
using var cts = new CancellationTokenSource();
_running = cts;
try
{
IReadOnlyList<PSObject> services = await GetRunningServicesAsync(cts.Token);
ResultList.ItemsSource = services
.Select(row => row.Properties["Name"]?.Value?.ToString() ?? "")
.ToList();
}
catch (PipelineStoppedException)
{
// Normaler Ablauf bei der Abbrechen-Schaltfläche oder beim Schließen des Fensters. Keine Ausgabe
}
catch (RuntimeException ex)
{
MessageBox.Show($"PowerShell failed: {ex.Message}");
}
finally
{
_running = null;
RunButton.IsEnabled = true;
}
}
private void CancelButton_Click(object sender, RoutedEventArgs e) => _running?.Cancel();
protected override void OnClosed(EventArgs e)
{
_running?.Cancel(); // Verhindert, dass die Pipeline im Hintergrund weiterläuft, obwohl das Fenster geschlossen wurde
base.OnClosed(e);
}
Vier Punkte sollten Sie sich merken.
- Direkt nach der Rückkehr von
awaitbefinden Sie sich sowohl bei WinForms als auch bei WPF wieder auf dem UI-Thread. Der Synchronisierungskontext stellt das sicher, daher sindInvokeoderDispatcher.Invokenicht nötig. - Deaktivieren Sie die Schaltfläche während der Ausführung. Tun Sie das nicht, läuft dieselbe Operation doppelt.
- Exceptions werden mit
RuntimeExceptionabgefangen. IstErrorActionnicht aufStopgesetzt, prüfen Sie zusätzlich den Fehlerstream (Kapitel 10). - Richten Sie unbedingt einen Stopp-Zugang ein. Halten Sie die
PowerShell-Instanz als lokale Variable in der Methode versteckt, kann die aufrufende SeiteStop()nicht aufrufen. Hängt der Befehl oder schließt der Benutzer das Fenster, entsteht ein Bildschirm, der mit deaktivierter Schaltfläche endlos weiterwartet.
Wird gestoppt, wirft das wartende InvokeAsync eine PipelineStoppedException. Fangen Sie sie, wie oben gezeigt, nicht als Fehlschlag, sondern als normalen Ablauf ab. Die Stopp-API von PowerShell bietet das synchrone Stop(), das asynchrone BeginStop / EndStop sowie StopAsync (siehe die Quellen am Ende). Da der Stopp vom UI-Thread aus erfolgen kann, wählen Sie die asynchrone Variante, die niemanden warten lässt.
Und: Haben Sie asynchron gestoppt, warten Sie vor dem Verwerfen dessen Abschluss ab. Dass await running zurückkehrt und das mit BeginStop begonnene Stoppen endet, geschieht unabhängig voneinander. Verlassen Sie den using PowerShell ps-Block trotzdem, läuft EndStop nach Dispose und greift auf die bereits verworfene Instanz zu. Und das geschieht in einem Threadpool-Callback, in dem es nirgendwo ein try gibt, das eine geworfene Exception auffängt ── die Anwendung stürzt kurz vor dem Beenden aus unerfindlichem Grund ab. Genau deshalb wird im obigen Code auf using verzichtet, stattdessen try / finally verwendet und das Stoppen zunächst als Task gehalten, bevor darauf awaitet wird. Durch das Einwickeln mit Task.Factory.FromAsync landet auch die Exception von EndStop in diesem Task und entkommt nicht mehr dem Threadpool.
Schreiben Sie für Windows PowerShell 5.1 und steht InvokeAsync nicht zur Verfügung, teilen Sie es in BeginInvoke / EndInvoke auf. Es gibt auch die Variante, die synchrone Invoke() mit Task.Run zu umschließen, aber das ist nur zulässig, wenn Sie auf Abbrechbarkeit verzichten. Task.Run befreit lediglich den UI-Thread; niemand rührt die laufende Pipeline an. Drückt der Benutzer „Abbrechen“, läuft der hängende Befehl im Hintergrund weiter.
using System.Management.Automation;
using System.Threading;
using System.Threading.Tasks;
private static async Task<IReadOnlyList<PSObject>> GetRunningServicesLegacyAsync(
CancellationToken cancellationToken)
{
PowerShell ps = PowerShell.Create();
Task? stopping = null;
try
{
ps.AddCommand("Get-Service")
.AddCommand("Select-Object")
.AddParameter("Property", new[] { "Name", "Status" });
// Reihenfolge und Aufräumen sind exakt dieselben wie im InvokeAsync-Beispiel
// oben. Erst abfangen, dann starten, dann den Stopp-Zugang anbringen.
// Beim synchronen Invoke() lässt sich „starten und danach registrieren“
// nicht schreiben, daher muss man es in BeginInvoke / EndInvoke aufteilen
cancellationToken.ThrowIfCancellationRequested();
// BeginInvoke / EndInvoke in einen Task einwickeln. Anders als bei
// Task.Run, das dauerhaft einen Threadpool-Thread wartend blockiert,
// wird der Abschluss hier von PowerShell selbst gemeldet
Task<PSDataCollection<PSObject>> running =
Task.Factory.FromAsync(ps.BeginInvoke(), ps.EndInvoke);
using (cancellationToken.Register(
state => Volatile.Write(ref stopping, StopAsync((PowerShell)state!)), ps))
{
// Bei einem Stopp: PipelineStoppedException
PSDataCollection<PSObject> output = await running;
return output.ToList();
}
}
finally
{
Task? pending = Volatile.Read(ref stopping);
if (pending is not null)
{
try { await pending; } catch { }
}
ps.Dispose();
}
}
Wickeln Sie es nicht zuerst mit Task.Run ein, um erst danach Register aufzurufen. Schreiben Sie innerhalb von Task.Run die Reihenfolge „abfangen → registrieren → Invoke()“, verpufft ein Abbruch, der zwischen Registrierung und Start eintrifft, wirkungslos. BeginStop trifft eine noch nicht laufende Pipeline und läuft ins Leere; danach startet Invoke() den Befehl, und das Stoppsignal ist bereits verbraucht ── genau dieselbe Falle, die im obigen InvokeAsync-Beispiel vermieden wurde, wiederholt sich hier unverändert.
In beiden Formen gilt derselbe Grundsatz: Kein langes Invoke() auf dem UI-Thread, die PowerShell aufrufende Methode fasst die UI nicht direkt an, und der Stopp-Zugang für die aufrufende Seite wird erst geöffnet, nachdem die Pipeline gestartet wurde – das sind die drei Punkte.
Parallelausführung und die Kosten des ersten Starts
Bei Verwaltungswerkzeugen kommt schnell die Anforderung auf: „Die Verarbeitung Rechner für Rechner ist zu langsam, wir wollen parallelisieren.“ Hier zunächst die Punkte, an denen man leicht hängen bleibt.
Zunächst gilt: Der erste Invoke()-Aufruf ist langsam. Das liegt daran, dass die Initialisierung des Runspace und die Modulsuche erst bei der ersten Ausführung stattfinden. Dass nur der eine Aufruf direkt nach dem Start Zeit braucht und die folgenden schnell sind, ist keine Anomalie. Wenn Sie messen, vergleichen Sie ohne den ersten Aufruf. Bei einer aus der Oberfläche aufgerufenen Anwendung bietet sich auch an, beim Start einmal einen leichten Befehl durchzuführen, um vorzuwärmen.
Zweitens: Teilen Sie eine einzelne PowerShell-Instanz nicht über mehrere Threads. Rufen Sie Invoke oder InvokeAsync an einer bereits laufenden Instanz erneut auf, entsteht eine InvalidOperationException mit der Begründung, der Befehl sei bereits gestartet. Möchten Sie parallel arbeiten, rufen Sie für jede Operation PowerShell.Create() auf.
Rufen Sie jedoch für jede Operation PowerShell.Create() auf, wird dabei jedes Mal ein Runspace angelegt. Nimmt die Anzahl zu, richten Sie einen RunspacePool ein und verwenden ihn wiederholt.
using System.Management.Automation;
using System.Management.Automation.Runspaces;
static async Task<IReadOnlyList<PSObject>> GetServiceAsync(
RunspacePool pool,
string serviceName)
{
using PowerShell ps = PowerShell.Create();
ps.RunspacePool = pool;
ps.AddCommand("Get-Service")
.AddParameter("Name", serviceName)
.AddCommand("Select-Object")
.AddParameter("Property", new[] { "Name", "Status" });
PSDataCollection<PSObject> output = await ps.InvokeAsync();
return output.ToList();
}
Auf der Verwendungsseite öffnen Sie den Pool und stoßen die Aufrufe parallel an.
using System.Management.Automation;
using System.Management.Automation.Runspaces;
string[] serviceNames = { "Spooler", "W32Time", "EventLog" };
using RunspacePool pool = RunspaceFactory.CreateRunspacePool(1, 4);
pool.Open();
IReadOnlyList<PSObject>[] results = await Task.WhenAll(
serviceNames.Select(name => GetServiceAsync(pool, name)));
foreach (IReadOnlyList<PSObject> rows in results)
{
foreach (PSObject row in rows)
{
Console.WriteLine($"{row.Properties["Name"]?.Value}: {row.Properties["Status"]?.Value}");
}
}
Hier wird Get-Service zur Veranschaulichung verwendet, stellen Sie sich in der Praxis aber jeweils einen zeitaufwendigen Befehl pro Ziel vor. Entscheidend ist, ps.RunspacePool statt ps.Runspace zu setzen und die PowerShell-Instanz selbst für jede Operation neu anzulegen und wieder zu verwerfen.
Abschließend: Eine größere Pool-Größe macht es nicht zwangsläufig schneller. Die tatsächliche Obergrenze wird von der Last des Zielservers, der Authentifizierung, dem Netzwerk und der Belastbarkeit des jeweiligen Moduls gegenüber Parallelausführung bestimmt. Beginnen Sie mit einem kleinen Wert, messen Sie tatsächlich nach, und erhöhen Sie erst dann.
Größe bei der Anwendungsverteilung
Microsoft.PowerShell.SDK ist praktisch, erhöht aber auch die Abhängigkeiten, die in der Anwendung enthalten sind. Bei einem kleinen Hilfsprogramm mag das tolerierbar sein, aber je nach Verteilungsform und Aktualisierungsmethode kann die Größe zum Problem werden. Es beruhigt, das frühzeitig mit der tatsächlich verwendeten Verteilungsmethode zu prüfen – ClickOnce, MSIX, eine einzelne exe-Datei, ein internes Verteilungswerkzeug und Ähnliches.
14. Die Vorteile, Objekte statt Zeichenketten zu empfangen
Zum Schluss: Warum diese Beharrung auf PSObject? PowerShell als externen Prozess zu starten und die Standardausgabe zu lesen, ist einfach.
PowerShell-Ausgabe
↓
Zeichenkette
↓
Split / regulärer Ausdruck / Substring
↓
C#-Wert
Diese Methode hängt jedoch vom Anzeigeformat ab. Sie bricht leicht durch Spaltenbreiten, Gebietsschema, Zeilenumbrüche, Leerzeichen, Fehlermeldungen und Trennzeichen innerhalb von Werten.
Mit dem PowerShell SDK ergibt sich dagegen folgender Ablauf:
PowerShell-Ausgabe
↓
PSObject
↓
Properties / BaseObject
↓
C#-Typ
Hierbei lassen sich Werte anhand der Datenstruktur statt des Anzeigeformats auslesen. Bei Geschäftsanwendungen und Verwaltungswerkzeugen ist der zweite Weg leichter zu warten.
15. Zusammenfassung
Wenn Sie PowerShell aus C# ausführen und mit dem Ergebnis arbeiten möchten, lohnt es sich, nicht nur powershell.exe zu starten und die Standardausgabe zu lesen, sondern die Verwendung des PowerShell SDK in Betracht zu ziehen.
Der grundlegende Ablauf ist:
Microsoft.PowerShell.SDK hinzufügen
↓
Mit PowerShell.Create() das Ausführungsobjekt erzeugen
↓
Mit AddCommand / AddParameter / AddScript die Verarbeitung zusammensetzen
↓
Mit Invoke() ausführen
↓
Als Collection<PSObject> empfangen
↓
Werte über BaseObject oder Properties auslesen
↓
In ein C#-DTO / record / class umwandeln
Für die Praxis besonders wichtig sind diese drei Punkte:
- Für die Weiterverarbeitung in C#
Select-Objectoder[pscustomobject]verwenden, nichtFormat-Table - Benutzereingaben nicht direkt in die
AddScript-Zeichenkette einbetten, sondern möglichst überAddParameterübergeben PSObjectan der Grenze behandeln und im Inneren der Anwendung in C#-Typen umwandeln
PowerShell ist stark bei der Windows-Verwaltung und der Nutzung vorhandener Ressourcen, C# ist stark beim Aufbau von Anwendungen, Oberflächen und typsicherer Geschäftslogik. Verbinden Sie beide geschickt, lässt sich ein bestehendes PowerShell-Skript erhalten und schrittweise zu einer .NET-Anwendung ausbauen.
Quellen
- Das vollständige Beispielcode-Paket zu diesem Artikel (Bibliothek, Demo, Unit-Tests) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/csharp-run-powershell-receive-objects
- Microsoft Learn: Schnellstart für den Windows PowerShell-Host
https://learn.microsoft.com/ja-jp/powershell/scripting/developer/hosting/windows-powershell-host-quickstart - Microsoft Learn: Befehle hinzufügen und aufrufen
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 - Microsoft Learn: PowerShell.InvokeAsync Method
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.powershell.invokeasync - Microsoft Learn: PowerShell.BeginStop Method (stoppt einen laufenden Befehl asynchron; das zurückgegebene
IAsyncResultwird mitEndStopentgegengenommen)
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.powershell.beginstop - Microsoft Learn: PowerShell.Stop Method (synchrone Variante; kehrt erst zurück, wenn das Stoppen abgeschlossen ist)
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.powershell.stop - Microsoft Learn: Mehrere Runspaces erstellen
https://learn.microsoft.com/ja-jp/powershell/scripting/developer/hosting/creating-multiple-runspaces - 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/
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
COM und .NET aus PowerShell aufrufen — Was Ihre Skripte erreichen können, erweitern
Ein praktischer Leitfaden zum Aufrufen von .NET-Klassen aus PowerShell, zum Einbetten von C#- und Win32-APIs mit Add-Type, zur Steuerung ...
WMI/CIM aus C# und PowerShell verwenden — Praxisleitfaden für Hardwareinformationen, Prozessüberwachung und Remoteabfragen
WMI/CIM ist die Standardlösung, um die Seriennummer eines PCs auszulesen, freien Festplattenspeicher zu überwachen und den Start von Proz...
Parameterdesign und Modularisierung für PowerShell-Skripte — Von der „funktionierenden“ zur „übergabefähigen“ Skript
Eine schrittweise Vorgehensweise, um ein PowerShell-Skript auf eine Qualität zu heben, die Sie an andere übergeben können. Behandelt den ...
Die Unterschiede zwischen Windows PowerShell 5.1 und PowerShell 7 ── Ein praktischer Leitfaden zur Migration interner Skripte
Das Verhältnis zwischen Windows PowerShell 5.1 und PowerShell 7 (Nebeneinander-Koexistenz und pwsh.exe), Microsofts offizielle Haltung, d...
Excel- und CSV-Arbeiten mit PowerShell automatisieren — Praxisrezepte für Auswertung, Abgleich und Berichtsausgabe
Praxisrezepte, um mit PowerShell die CSV-Auswertung, den Abgleich und die Excel-Berichtsausgabe zu automatisieren. Behandelt die Standard...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Welche Methode ist am besten geeignet, um PowerShell aus C# auszuführen?
- Grob gibt es zwei Wege: über ProcessStartInfo powershell.exe/pwsh.exe als externen Prozess zu starten, oder System.Management.Automation.PowerShell (das PowerShell SDK) zu verwenden. Wenn Sie nur die Standardausgabe als Zeichenkette lesen wollen, funktioniert auch der erste Weg, aber für Verwaltungswerkzeuge oder Geschäftsanwendungen, die das Ergebnis auf der C#-Seite weiterverarbeiten, eignet sich der zweite Weg besser, weil er das Ergebnis als Sammlung von PSObject liefert. Das Parsen von Zeichenketten entfällt, und Sie können eine Verarbeitung schreiben, die nicht vom Anzeigeformat abhängt und dadurch sicherer ist.
- Wie setzt man BaseObject und Properties von PSObject sinnvoll ein?
- BaseObject verwenden Sie, wenn Sie das von PowerShell zurückgegebene ursprüngliche .NET-Objekt unverändert nutzen möchten. Führen Sie zum Beispiel Get-Process direkt aus, lässt sich das Ergebnis als System.Diagnostics.Process abrufen. Wurden die Spalten dagegen mit Select-Object oder [pscustomobject] zurechtgeschnitten, kommt das Ergebnis meist als PowerShell-Custom-Objekt zurück; in diesem Fall ist es natürlicher, den Wert über Properties["Spaltenname"]?.Value mit dem Spaltennamen abzurufen.
- Worauf muss ich achten, wenn ich Benutzereingaben von C# an PowerShell übergebe?
- Sie sollten es vermeiden, Benutzereingaben durch Zeichenkettenverkettung in das AddScript-Skript einzubetten, weil die Eingabe dabei als PowerShell-Code interpretiert werden könnte – das ist gefährlich. Um Werte zu übergeben, verwenden Sie AddCommand und AddParameter; dabei wird der Wert nicht als Code-Zeichenkette, sondern als Parameterwert behandelt. Es ist ratsam, AddScript auf feste kurze Skripte oder das Laden bestehender Skripte zu beschränken.
- Warum darf man Format-Table nicht verwenden, wenn man das PowerShell-Ergebnis in C# empfängt?
- Weil das Ergebnis nach Format-Table oder Format-List nicht mehr das ursprüngliche Objekt ist, sondern Formatierungsinformationen für die Anzeige, sodass sich auf der C#-Seite keine Werte mehr als Eigenschaften abrufen lassen. Wenn Sie das Ergebnis in C# weiterverarbeiten möchten, schränken Sie die Spalten mit Select-Object ein oder lassen Sie die PowerShell-Seite ein [pscustomobject] zurückgeben. Die Faustregel lautet: „Nur für die Bildschirmanzeige die Format-Familie, für die Übergabe an C# Select-Object.“
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.