طريقة تشغيل PowerShell من C# (CSharp) واستقبال النتيجة ككائن

· آخر تحديث: · · C#, CSharp, PowerShell, Windows, .NET, الأتمتة, الاستفادة من الأصول القائمة

الرغبة في تشغيل PowerShell من C# موقف شائع في تطبيقات الأعمال والأدوات الداخليّة. مثلًا، في المعالجات التالية.

  • الحصول على قائمة خدمات Windows
  • فحص العمليّات أو سجلّات الأحداث
  • استدعاء سكربت PowerShell قائم من تطبيق C#
  • تشغيل أمر PowerShell من أداة GUI صغيرة موجَّهة للمدراء
  • نقل أصول الأتمتة القائمة في جانب PowerShell إلى تطبيق .NET تدريجيًّا

إذا اكتُفي بالتشغيل البسيط، فحتّى طريقة تشغيل powershell.exe أو pwsh.exe كعمليّة خارجيّة وقراءة المخرَج القياسيّ كنصّ تعمل. لكن بهذه الطريقة، تُفقَد ميزة PowerShell وهي «خطّ أنابيب الكائنات (object pipeline)».

نتيجة PowerShell في الأصل ليست مجرّد نصّ. فنتيجة Get-Process هي كائن عمليّة، ونتيجة Get-Service هي كائن خدمة. إذا أمكن استقبالها في جانب C# مع الحفاظ على تلك البنية، تُغنيك عن تحليل النصّ، وتصبح المعالجة أكثر أمانًا بكثير.

في هذا المقال نرتّب أساسيّات تشغيل PowerShell من C# واستقبال النتيجة ككائن PSObject.

يُذكَر أنّ الكود الوارد في هذا المقال منشور على GitHub كمجموعة عيّنة كاملة قابلة للبناء والتشغيل (غلاف تنفيذ ومكتبة معالجة تحويل، وعرض توضيحيّ لسطر الأوامر يُجسِّد كلّ فصل من المقال، واختبارات وحدة تتحقّق من استقبال PSObject ومعالجة الأخطاء).

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

1. استخدم PowerShell SDK بدل تشغيل عمليّة خارجيّة

توجد طريقتان رئيسيّتان لاستدعاء PowerShell من C#.

الطريقة الخصائص الموقف المناسب
تشغيل powershell.exe / pwsh.exe عبر ProcessStartInfo قراءة المخرَج القياسيّ والخطأ القياسيّ كنصّ تشغيل بسيط لدفعة قائمة، معالجة الاكتفاء بترك سجلّ
استخدام System.Management.Automation.PowerShell يمكن استقبال النتيجة ككائن PSObject معالجة تعديل النتيجة في جانب C#، أدوات الإدارة، تطبيقات الأعمال

ما يتناوله هذا المقال هو الطريقة الثانية. باستخدام System.Management.Automation.PowerShell، يمكن تركيب وتشغيل خطّ أنابيب PowerShell من كود C#. المهمّ هو أنّ القيمة المُعادة ليست نصًّا، بل أساسًا Collection<PSObject>.

بعبارة أخرى، يصبح التفكير على هذا النحو.

تنفيذ أمر PowerShell
  ↓
استقبال النتيجة كمجموعة من PSObject
  ↓
استخراج القيمة من BaseObject أو Properties
  ↓
التحويل عند الحاجة إلى DTO / record / class بلغة C#

النقطة الجوهريّة هي معاملة مخرَج PowerShell ككائن منذ البداية، لا تفكيكه كنصّ.

2. البيئة المفترَضة

نضرب في هذا المقال مثالًا بتطبيق سطر أوامر بلغة .NET 8. تختلف نسخة .NET المستهدَفة باختلاف نسخة PowerShell SDK، لذا اختر بما يناسب إطار العمل المستهدَف في مشروعك.

حتّى يونيو 2026، من المفيد التفكير على النحو التالي كمثال.

الإطار المستهدَف لتطبيق 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");
    }
}

هناك ثلاث نقاط ينبغي الانتباه إليها هنا: إنشاء كائن تنفيذ PowerShell عبر PowerShell.Create()، وتركيب الأمر والمُعطى (parameter) عبر AddCommand("Get-Process") وAddParameter("Id", currentProcessId)، وأنّ القيمة المُعادة من Invoke() هي Collection<PSObject>.

PSObject غلاف يُحيط بالقيمة التي يُخرِجها PowerShell. إذا أردت رؤية كائن .NET الأصليّ الموجود بداخله، انظر إلى BaseObject. في هذا المثال، يمكن استخراج محتوى نتيجة Get-Process كـSystem.Diagnostics.Process.

4. التمييز بين استخدام BaseObject وProperties

عند التعامل مع نتيجة PowerShell في C#، أوّل ما يُتردَّد بشأنه هو هذان الاثنان.

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

معيار الاختيار بينهما كالتالي.

طريقة الاستخراج الموقف المناسب
BaseObject عندما تريد استخدام كائن .NET الأصليّ الذي أعادته PowerShell كما هو
Properties["..."] عندما تريد استخراج عمود أنشأته عبر Select-Object أو [pscustomobject]

عند تنفيذ أمر مثل Get-Process مباشرةً، قد يحتوي BaseObject على كائن .NET الأصليّ. أمّا إذا جرى تنسيق الأعمدة في جانب PowerShell عبر Select-Object، فغالبًا ما تُعاد النتيجة ككائن مخصّص من PowerShell. في هذه الحالة، يكون استخراج القيمة من Properties عبر اسم العمود أكثر طبيعيّةً.

5. قراءة نتيجة Select-Object في C#

في العمل الفعليّ، نادرًا ما تحتاج إلى جميع خصائص PowerShell المُعادة. إذا أردت تمرير الأعمدة اللازمة فقط إلى جانب C#، استخدم Select-Object داخل خطّ أنابيب PowerShell.

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#، يُنشَأ خطّ أنابيب PowerShell عبر استدعاء AddCommand تباعًا.

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

بهذه الكتابة، يُمرَّر مخرَج الأمر السابق إلى الأمر التالي.

بعد تضييق الأعمدة عبر Select-Object، تُستخرَج القيمة باسم العمود كـrow.Properties["Name"]?.Value.

6. التحويل إلى record بلغة C#

إذا مرَّرت 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، وحوِّلها داخل التطبيق إلى نوع C# عاديّ مثل ProcessSummary.

هذا الفصل، عند إجرائه، يقلِّل نطاق الأثر لاحقًا حتّى لو غيّرت أمر 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، استخدم قدر الإمكان AddCommand وAddParameter.

string userInputPath = GetPathFromUser();

using PowerShell ps = PowerShell.Create();

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

القيمة المُمرَّرة عبر AddParameter تُعامَل كقيمة مُعطى (parameter)، لا تُدمَج كسلسلة كود PowerShell.

في العمل الفعليّ، يُنصَح بالتمييز التالي.

الأسلوب الموقف المناسب
AddCommand / AddParameter عند إرادة تركيب أمر بأمان من جانب C#
AddScript عند تنفيذ سكربت قصير ثابت، أو تحميل سكربت قائم
AddScript مع تسلسل نصّيّ يُتجنَّب من حيث المبدأ. إن استُخدِم، فليكن مع تدقيق قيمة الإدخال وتهريبها (escaping) بحذر شديد

عند دمج PowerShell في C#، يصبح بمقدور التطبيق عمليّات قويّة. لكن مقابل هذه الفائدة، يلزم الالتزام بخطّ أحمر واحد وهو عدم تحويل إدخال المستخدم إلى سكربت كما هو.

9. Format-Table للعرض على الشاشة فقط في النهاية. لا تستخدمه قبل التمرير إلى C#

إذا أردت استقبال نتيجة PowerShell ككائن في C#، فلا تستخدم Format-Table أو Format-List من حيث المبدأ.

مثلًا، سكربت PowerShell التالي مفيد لإنسان يشاهده على الشاشة.

Get-Service | Format-Table Name, Status

لكن إذا استُخدِمت Format-Table قبل الاستقبال في جانب C#، تتحوّل النتيجة من كائن الخدمة إلى معلومات تنسيق مخصّصة للعرض. إذا أردت التعامل معها في 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، المخرَج والخطأ تدفّقان (streams) منفصلان. إذا اكتفيت بمراقبة القيمة المُعادة من 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 (cmdlets) أخطاء توقف المعالجة، وأخطاء تستمرّ فيها المعالجة. إذا أردت التعامل معها كاستثناء في جانب C#، توجد طريقة تحديد Stop لـ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}");
}

أيّهما أفضل يعتمد على طبيعة التطبيق. في أداة إدارة تريد فيها «عرض القائمة حتّى مع فشل جزء منها»، فجمع تدفّق الخطأ وعرضه على الشاشة أنسب، وإذا أردت «إيقاف المعالجة بأكملها عند الفشل»، فمعاملتها كاستثناء عبر 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# قد يصبح أقلّ قابليّةً للقراءة قليلًا. الأوامر والمُعطيات (parameters) البسيطة يمكن كتابتها عبر AddCommand / AddParameter، لكنّ عمليّات التصفية أو التجميع المعقّدة قد يكون من الأسهل قراءتها إذا حُضِّرت كسكربت PowerShell ثابت. حتّى في تلك الحالة، لا تتغيّر السياسة القاضية بعدم دمج الإدخال الخارجيّ مباشرةً في سلسلة السكربت.

12. المعالجة المعقّدة تُرتَّب في كائن من جانب PowerShell

عند الجمع بين C# وPowerShell، يسهل التصميم إذا وُزِّعت المسؤوليّة عن ما يقوم به كلّ طرف.

التوزيع المُوصى به كالتالي.

الجهة المسؤولة ما تقوم به
PowerShell العمليّات القريبة من Windows والوحدات، والسكربتات القائمة، وتنفيذ أوامر الإدارة
C# واجهة المستخدم، والتحقّق من الإدخال، وتحويل الأنواع، ومنطق الأعمال، والحفظ، والتكامل مع واجهات 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. نقاط انتباه شائعة في العمل الفعليّ

عند تشغيل PowerShell من C#، لا يكفي أن يعمل الكود فحسب. في العمل الفعليّ، من الآمن التحقّق مبكّرًا من النقاط التالية.

صلاحيّات المستخدم المُنفِّذ

يعمل PowerShell بصلاحيّات المستخدم الذي يُشغِّل تطبيق C#. الأوامر التي تحتاج صلاحيّة مدير تفشل إن نُفِّذت بمستخدم عاديّ. تحتاج وحدات عمليّات الخدمات، وسجلّ الأحداث، والشهادات، والسجلّ (registry)، وHyper-V، ووحدات إدارة Microsoft 365 وغيرها، إلى فصل واضح للصلاحيّات.

الفرق بين 32bit و64bit

في Windows، قد يختلف السجلّ والوحدات المرئيّة بين عمليّة 32bit وعمليّة 64bit. إذا كنت تبنيه كأداة إدارة لـWindows، فمن الأفضل من حيث المبدأ افتراض التشغيل بصيغة x64 لتقليل المشكلات.

وجود الوحدة في بيئة التشغيل

حتّى مع إضافة PowerShell SDK إلى تطبيق C#، لا تُضاف جميع وحدات PowerShell تلقائيًّا. مثلًا، عند استخدام وحدة إدارة منتج معيّن أو وحدة داخليّة، يلزم التحقّق من وجود تلك الوحدة في بيئة التشغيل ومن أيّ مسار تُحمَّل.

لا توقف خيط واجهة المستخدم في تطبيقات الشاشة

عند تشغيل PowerShell من WinForms أو WPF، فإنّ تنفيذ معالجة ثقيلة مباشرةً على خيط واجهة المستخدم يُجمِّد الشاشة. في هذه الحالة، صمِّم التنفيذ كمعالجة خلفيّة، وحدِّث واجهة المستخدم بعد الاكتمال. يتضمّن PowerShell SDK أيضًا واجهات تنفيذ غير متزامن، لكن ابدأ أوّلًا بالالتزام بسياسة «عدم تنفيذ Invoke() طويل المدّة على خيط واجهة المستخدم».

حجم التوزيع عند نشر التطبيق

Microsoft.PowerShell.SDK مفيد، لكنّه يزيد أيضًا من الاعتماديّات المضمَّنة في التطبيق. قد يكون ذلك مقبولًا في أداة صغيرة، لكن بحسب صيغة التوزيع وطريقة التحديث، قد يصبح الحجم مصدر قلق. سواء كان التوزيع عبر ClickOnce أو MSIX أو exe منفرد أو أداة توزيع داخليّة، فمن الآمن التحقّق مبكّرًا في صيغة التوزيع الفعليّة.

14. فائدة الاستقبال ككائن بدل نصّ

أخيرًا، لماذا كلّ هذا الإصرار على PSObject؟ إنّ طريقة تشغيل PowerShell عبر عمليّة خارجيّة وقراءة المخرَج القياسيّ بسيطة.

مخرَج PowerShell
  ↓
نصّ
  ↓
Split / تعبير نمطيّ / Substring
  ↓
قيمة C#

لكن هذه الطريقة تعتمد على شكل العرض. وتصبح هشّة بتأثير عرض العمود، والإعداد المحليّ (locale)، والسطر الجديد، والمسافة، ورسالة الخطأ، والفاصل داخل القيمة.

أمّا باستخدام PowerShell SDK، فيصبح التدفّق على النحو التالي.

مخرَج PowerShell
  ↓
PSObject
  ↓
Properties / BaseObject
  ↓
نوع C#

هذه الطريقة تستخرج القيمة بناءً على بنية البيانات لا شكل العرض. في تطبيقات الأعمال وأدوات الإدارة، الطريقة الثانية أسهل صيانةً.

15. الخلاصة

إذا أردت تشغيل PowerShell من C# والتعامل مع نتيجته، فمن المفيد النظر في استخدام PowerShell SDK، لا الاكتفاء بتشغيل powershell.exe وقراءة المخرَج القياسيّ.

التدفّق الأساسيّ كالتالي.

إضافة Microsoft.PowerShell.SDK
  ↓
إنشاء كائن تنفيذ عبر PowerShell.Create()
  ↓
تركيب المعالجة عبر AddCommand / AddParameter / AddScript
  ↓
التنفيذ عبر Invoke()
  ↓
الاستقبال كـCollection<PSObject>
  ↓
استخراج القيمة من BaseObject أو Properties
  ↓
التحويل إلى DTO / record / class بلغة C#

النقاط الثلاث المهمّة بشكل خاصّ في العمل الفعليّ هي هذه.

  • إذا استُخدِمت في معالجة لاحقة بلغة C#، استخدم Select-Object أو [pscustomobject] بدل Format-Table
  • لا تُضمِّن إدخال المستخدم مباشرةً في نصّ 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 https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.powershell
  • Microsoft Learn: صنف PSObject 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/

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

ما الطريقة المناسبة لتشغيل PowerShell من C#؟
بشكل عامّ، توجد طريقتان: تشغيل powershell.exe/pwsh.exe كعمليّة خارجيّة عبر ProcessStartInfo، واستخدام System.Management.Automation.PowerShell (‏PowerShell SDK). إذا اكتفيت بقراءة المخرَج القياسيّ كنصّ، تعمل الطريقة الأولى أيضًا، لكن في أدوات الإدارة أو تطبيقات الأعمال التي تعالج النتيجة في جانب C#، تناسب الطريقة الثانية أكثر، إذ يمكن استقبال النتيجة كمجموعة من PSObject. فتُغنيك عن تحليل النصّ، ويمكنك كتابة معالجة آمنة لا تعتمد على شكل العرض.
كيف نميّز بين استخدام BaseObject وProperties في PSObject؟
يُستخدَم BaseObject عندما تريد استخدام كائن .NET الأصليّ الذي أعادته PowerShell كما هو. فمثلًا، عند تنفيذ Get-Process مباشرةً، يمكن استخراج النتيجة كـSystem.Diagnostics.Process. أمّا نتيجة تنسيق الأعمدة عبر Select-Object أو [pscustomobject] فغالبًا ما تُعاد ككائن مخصّص من PowerShell، لذا يكون استخراجها عبر Properties["اسم العمود"]؟.Value بتحديد اسم العمود أكثر ملاءمة.
ما الذي ينبغي الانتباه إليه عند تمرير إدخال المستخدم من C# إلى PowerShell؟
ينبغي تجنّب دمج إدخال المستخدم في نصّ السكربت الممرَّر إلى AddScript عبر التسلسل النصّيّ. فهذا خطير لأنّه يترك مجالًا لتفسير الإدخال ككود PowerShell. عند تمرير قيمة، يؤدّي استخدام AddCommand وAddParameter إلى معاملة القيمة كقيمة مُعطى (parameter) لا كسلسلة كود. من الحكمة قصر AddScript على سكربتات قصيرة ثابتة أو تحميل سكربتات قائمة.
لماذا لا يجوز استخدام Format-Table عند استقبال نتيجة PowerShell في C#؟
لأنّ تمرير النتيجة عبر Format-Table أو Format-List يحوّلها من الكائن الأصليّ إلى معلومات تنسيق مخصّصة للعرض، فيتعذّر استخراج القيم كخصائص في جانب C#. عند استخدامها في معالجة لاحقة بلغة C#، ضيِّق الأعمدة عبر Select-Object، أو رتِّب جانب PowerShell لإعادة [pscustomobject]. القاعدة هي: «للعرض على الشاشة فقط استخدم Format، ولتمريرها إلى C# استخدم Select-Object».

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

روابط عامة

العودة إلى المدونة