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

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

سجل التعديلات (2 تحديثات، آخر تحديث 2 Sep، 2026)

سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.

أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621544)

هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.

غو كومورا (2026). طريقة تشغيل PowerShell من C# (CSharp) واستقبال النتيجة ككائن. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621544 https://comcomponent.com/ar/blog/2026/06/08/001-csharp-run-powershell-receive-objects/

DOI (أحدث نسخة)
10.5281/zenodo.21621544
DOI (هذه النسخة)
10.5281/zenodo.22240951

الرغبة في تشغيل 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 ككائن منذ البداية، لا تفكيكه كنصّ.

في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 26، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle

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

عند الكتابة لـ .NET Framework، ما الذي يختلف؟

قد يكون الهدف المحافَظ عليه تطبيق Windows Forms أو WPF على .NET Framework، وتريد استدعاء PowerShell منه. أمثلة الشيفرة في هذا المقال تفترض .NET الحالي، لكن أسلوب الكتابة في جانب C# يكاد يكون نفسه. ما يختلف هو النقاط التالية.

الزاوية .NET الحالي + Microsoft.PowerShell.SDK .NET Framework + Microsoft.PowerShell.5.1.ReferenceAssemblies
PowerShell الذي يُنفَّذ PowerShell سلسلة 7 المضمَّنة في الحزمة Windows PowerShell 5.1 المضمَّن في Windows
دور حزمة NuGet تتضمّن الكيان تجميعات مرجعيّة فقط. تجميعات وقت التشغيل من جهة نظام التشغيل
الصيغة والأوامر المتاحة نطاق PowerShell سلسلة 7 نطاق 5.1. لا تُستخدم ForEach-Object -Parallel ولا ?? ولا العامل الثلاثيّ
مسار البحث عن الوحدات $env:PSModulePath لـ PowerShell 7 $env:PSModulePath لـ Windows PowerShell
التنفيذ غير المتزامن يمكن استخدام InvokeAsync BeginInvoke / EndInvoke، أو لفّ Invoke() بـ Task.Run
أسلوب الكتابة في جانب C# PowerShell.Create()، AddCommand، AddParameter، Invoke()، Collection<PSObject> نفسه
حجم التوزيع كبير لأنّ طقم SDK يُضمَّن صغير لأنّه يستخدم جهة نظام التشغيل

يصبح ملفّ .csproj على النحو التالي مثلًا.

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

ما يؤثّر أكثر في العمل الفعليّ هو أنّ ما يُنفَّذ هو Windows PowerShell 5.1. عند إدخال سكربت PowerShell قائم، إن كان مكتوباً لـ PowerShell 7 فقد يحدث خطأ صياغة في 5.1. وبالعكس، قد تختار هذا المسار لأنّك تريد وحدة قديمة لا تعمل إلا في Windows PowerShell 5.1.

شيفرة الفصول من 3 إلى 12 في هذا المقال لا تستخدم InvokeAsync، لذا تعمل كما هي أيضاً في جانب .NET Framework. محيط التحويل إلى غير متزامن وحده نعود إليه في الفصل 13.

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 واجهة تنفيذ غير متزامن، واستخدامها هو الأكثر استقامة. PowerShell.InvokeAsync يعيد Task<PSDataCollection<PSObject>>، لذا يمكن await مباشرة.

أوّلاً، استخرج جزء استدعاء PowerShell كمنهج لا يلمس واجهة المستخدم.

using System.Management.Automation;

// يُفترَض وضع المنهجين التاليين داخل صنف النافذة أو النموذج.
//
// استقبل cancellationToken دائماً. إن أخفيت مثيل PowerShell في متغيّر محلّي
// داخل هذا المنهج، فلن يملك المستدعي وسيلة لاستدعاء Stop().
// عندما يتجمّد الأمر أو يغلق المستخدم الشاشة، يبقى المعالج الذي ينتظر
// await مع الزرّ معطّلاً
private static async Task<IReadOnlyList<PSObject>> GetRunningServicesAsync(
    CancellationToken cancellationToken)
{
    // لا نستخدم using. يلزم انتظار اكتمال الإيقاف قبل التخلّص (لاحقاً)
    PowerShell ps = PowerShell.Create();
    Task? stopping = null;

    try
    {
        // `-EQ` مفتاح لا يأخذ قيمة، والقيمة المقارَنة تُمرَّر إلى `-Value`
        // (الصيغة المختصرة هي `-Property <String> -EQ -Value <Object>`).
        // كتابة AddParameter("EQ", "Running") تفشل عند تخصيص الوسيط
        ps.AddCommand("Get-Service")
          .AddCommand("Where-Object")
              .AddParameter("Property", "Status")
              .AddParameter("EQ")
              .AddParameter("Value", "Running")
          .AddCommand("Select-Object")
              .AddParameter("Property", new[] { "Name", "DisplayName", "Status" });

        // لا تسجّل قبل البدء. إن كان الرمز الممرَّر ملغى مسبقاً، ينفّذ Register
        // ردّ النداء في الحال. يطير BeginStop إلى مسار لم يبدأ بعد فيُفلِت،
        // ثمّ يبدأ InvokeAsync الأمر، وتكون إشارة الإيقاف قد استُهلكت —
        // فيتكوّن أمر يواصل الجري حتّى بعد إغلاق النافذة.
        // ارفض أوّلاً، ثمّ شغّل، ثمّ ركّب فم الإيقاف
        cancellationToken.ThrowIfCancellationRequested();

        Task<PSDataCollection<PSObject>> running = ps.InvokeAsync();

        // عند الإلغاء أوقف المسار. Stop() لا يعود حتّى ينتهي الإيقاف،
        // وقد يُستدعى الإلغاء من خيط واجهة المستخدم، لذا نستخدم النسخة غير المتزامنة.
        // نستقبل الإيقاف نفسه كـ Task وننتظر اكتماله في finally أدناه
        using (cancellationToken.Register(
            state => Volatile.Write(ref stopping, StopAsync((PowerShell)state!)), ps))
        {
            PSDataCollection<PSObject> output = await running;
            return output.ToList();
        }
    }
    finally
    {
        // إن طُلب الإيقاف، انتظر اكتماله ثمّ تخلّص.
        // عودة await running وانتهاء الإيقاف يحدثان منفصلين، فإن تخلّصت
        // هنا دون انتظار، يلمس EndStop الذي يجري لاحقاً PowerShell متخلَّصاً منه.
        // وهو ردّ نداء في مجمع الخيوط، فلا مكان لاستقبال الاستثناء المرمي
        // (يسقط العمليّة كلّها)
        Task? pending = Volatile.Read(ref stopping);
        if (pending is not null)
        {
            try { await pending; }
            catch { /* لا تغطِّ النتيجة أو الاستثناء الأصلي بفشل جهة الإيقاف */ }
        }

        ps.Dispose();
    }
}

// نلفّ BeginStop / EndStop في Task. استدعاء EndStop مباشرة داخل ردّ النداء
// يسرّب استثناءه من مجمع الخيوط
private static Task StopAsync(PowerShell ps) =>
    Task.Factory.FromAsync(ps.BeginStop, ps.EndStop, null);

جهة الاستدعاء هي معالج حدث النافذة أو النموذج. هنا يجوز async void. معالج الحدث من الأماكن القليلة التي يُسمَح فيها بـ async void.

// يُفترَض الوضع في صنف نافذة WPF.
// في WinForms اقرأ RoutedEventArgs كـ EventArgs،
// وIsEnabled كـ Enabled، وItemsSource كـ DataSource.
// للإلغاء أثناء التشغيل. يُستخدم لزرّ الإيقاف وعند إغلاق النافذة معاً
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)
    {
        // مسار طبيعي عند زرّ الإيقاف أو إغلاق النافذة. لا تُخرج شيئاً
    }
    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();   // لا تُبقِ مساراً يجري في الخلف بعد الإغلاق
    base.OnClosed(e);
}

هناك أربع نقاط ينبغي إدراكها.

  1. الموضع الذي يعود إليه await هو خيط واجهة المستخدم في WinForms وفي WPF. سياق المزامنة يعيدك، فلا حاجة إلى Invoke أو Dispatcher.Invoke.
  2. عطّل الزرّ أثناء التنفيذ. إن لم تعطّله، تشغّل المعالجة نفسها مرّتين.
  3. استقبل الاستثناء بـ RuntimeException. إن لم تجعل ErrorAction هو Stop، راجع أيضاً تدفّق الأخطاء (الفصل 10).
  4. جهّز دائماً فم إيقاف. إن أخفيت مثيل PowerShell في متغيّر محلّي داخل المنهج، فلن يملك المستدعي وسيلة لاستدعاء Stop(). عندما يتجمّد الأمر أو يغلق المستخدم النافذة، تبقى الشاشة تنتظر والزرّ معطّل.

عند الإيقاف، يرمي InvokeAsync المنتظَر PipelineStoppedException. كما أعلاه، امسكه كمسار طبيعي لا كفشل. واجهة إيقاف PowerShell تتضمّن Stop() المتزامن، وBeginStop / EndStop غير المتزامن، وStopAsync (راجع المعلومات المرجعيّة في النهاية). قد توقف من خيط واجهة المستخدم، لذا اختر النسخة غير المتزامنة التي لا تُنتظر.

وإن أوقفت بشكل غير متزامن، فانتظر اكتمال ذلك قبل التخلّص. عودة await running وانتهاء الإيقاف الذي بدأ بـ BeginStop يحدثان منفصلين. إن خرجت مع using PowerShell ps، يجري EndStop بعد Dispose، فيلمس مثيلاً متخلَّصاً منه. وهناك ردّ نداء مجمع الخيوط، ولا try يستقبل الاستثناء المرمي — يسقط التطبيق قبيل الانتهاء لسبب مجهول. لهذا ألغينا using في الشيفرة أعلاه وجعلناها try / finally، وأمسكنا الإيقاف كـ Task ثمّ await. اللفّ بـ Task.Factory.FromAsync يُدخل استثناء EndStop أيضاً في ذلك Task، فلا يتسرّب من مجمع الخيوط.

إن كنت تكتب لـ Windows PowerShell 5.1 ولا يتاح InvokeAsync، فاقسم إلى BeginInvoke / EndInvoke. توجد أيضاً كتابة تلفّ Invoke() المتزامن بـ Task.Run، لكنّها مقصورة على حالة التخلّي عن الإلغاء. Task.Run يحرّر خيط واجهة المستخدم فقط، ولا يلمس أحد المسار الجاري. حتّى لو ضغط المستخدم «إلغاء»، يواصل الأمر المتجمّد الجري في الخلف.

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

        // الترتيب والتنظيف هما نفس مثال InvokeAsync أعلاه تماماً.
        // ارفض أوّلاً، شغّل، ثمّ ركّب فم الإيقاف.
        // مع Invoke() المتزامن لا يمكن كتابة «شغّل ثمّ سجّل»،
        // لذا يلزم التقسيم إلى BeginInvoke / EndInvoke
        cancellationToken.ThrowIfCancellationRequested();

        // لفّ BeginInvoke / EndInvoke في Task. بخلاف Task.Run الذي يُبقي خيطاً
        // من مجمع الخيوط منتظراً، يُبلَّغ الاكتمال من جهة PowerShell
        Task<PSDataCollection<PSObject>> running =
            Task.Factory.FromAsync(ps.BeginInvoke(), ps.EndInvoke);

        using (cancellationToken.Register(
            state => Volatile.Write(ref stopping, StopAsync((PowerShell)state!)), ps))
        {
            // عند الإيقاف 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();
    }
}

لا تستخدم شكلاً يلفّ بـ Task.Run ثمّ Register. إن كتبت داخل Task.Run «ارفض → سجّل → Invoke()»، يُفلِت الإلغاء الذي يأتي بين التسجيل والبدء. يطير BeginStop إلى مسار لم يجرِ بعد، ثمّ يبدأ Invoke() الأمر، وتكون إشارة الإيقاف قد استُهلكت — نفس الفخّ الذي تجنّبناه في مثال InvokeAsync أعلاه يتكرّر كما هو.

في أيّ شكل، السياسة التي تُحفَظ واحدة. لا تنفّذ Invoke() طويلاً على خيط واجهة المستخدم، لا تلمس واجهة المستخدم مباشرة من منهج استدعاء PowerShell، وافتح فم إيقاف يستطيعه المستدعي بعد بدء المسار، هذه الثلاث.

التنفيذ المتوازي وتكلفة البدء الأولى

في أدوات الإدارة يظهر سريعاً طلب «المعالجة جهازاً جهازاً بطيئة، نريد التوازي». نذكر أوّلاً النقاط التي يسهل التعثّر فيها هنا.

أوّلاً، لـ أوّل Invoke() طبيعة أنّه بطيء. لأنّ تهيئة Runspace والبحث عن الوحدات يجريان عند التنفيذ الأوّل. أن يستغرق الأمر وقتاً في المرّة الأولى فقط بعد البدء، ثمّ يسرع من المرّة الثانية، ليس أمراً شاذاً. عند القياس، قارن بعد استبعاد المرّة الأولى. في تطبيق يُستدعى من الشاشة، توجد أيضاً حيلة تشغيل أمر خفيف مرّة واحدة عند البدء للإحماء.

ثانياً، لا تشارك مثيل PowerShell واحداً بين عدّة خيوط. إن استدعيت Invoke أو InvokeAsync مرّة أخرى على مثيل قيد التشغيل، يحدث InvalidOperationException بسبب «الأمر قد بدأ بالفعل». إن أردت التشغيل بالتوازي، فنفّذ PowerShell.Create() لكلّ عمليّة.

غير أنّ PowerShell.Create() لكلّ عمليّة ينشئ Runspace في كلّ مرّة. إن زاد العدد، جهّز RunspacePool وأعد الاستخدام منه.

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

في جهة الاستخدام، افتح المجمّع ثمّ ارمِ بالتوازي.

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

نستخدم Get-Service هنا للشرح، لكن افترض في الواقع أمراً يستغرق وقتاً لكلّ هدف. النقطة هي الضبط على ps.RunspacePool لا ps.Runspace، وإنشاء مثيل PowerShell نفسه لكلّ عمليّة ثمّ التخلّص منه.

أخيراً، رفع حجم المجمّع لا يعني بالضرورة التسريع. الحدّ الفعليّ تحدّده حمولة الخادم المستهدَف، ومعالجة المصادقة، والشبكة، وتحمّل الوحدة المستهدَفة للتنفيذ المتوازي. ابدأ بقيمة صغيرة، وقِس فعليّاً، ثمّ ارفع.

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

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/en-us/powershell/scripting/developer/hosting/windows-powershell-host-quickstart
  • Microsoft Learn: إضافة الأوامر واستدعاؤها https://learn.microsoft.com/en-us/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/en-us/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 (إيقاف الأمر الجاري بشكل غير متزامن. IAsyncResult المعاد يُستقبَل بـ EndStop) https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.powershell.beginstop
  • Microsoft Learn: PowerShell.Stop Method (النسخة المتزامنة. لا يعود حتّى ينتهي الإيقاف) https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.powershell.stop
  • Microsoft Learn: إنشاء عدّة فضاءات تنفيذ https://learn.microsoft.com/en-us/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/

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

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

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

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

ما الطريقة المناسبة لتشغيل 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».

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

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

غو كومورا

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

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

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