لماذا نستخدم Generic Host و BackgroundService في تطبيق سطح المكتب
· آخر تحديث: · 小村 豪 · C#, .NET, Generic Host, BackgroundService, WPF, WinForms, تطوير Windows, تصميم
سجل التعديلات (3 تحديثات، آخر تحديث 3 Sep، 2026)
سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.
- أُضيفت روابط الاستشارة الموجودة في الأصل الياباني (consultation_services). ولم يتغيّر نصّ المقالة نفسه. قراءة النسخة السابقة لهذا التحديث (DOI: 10.5281/zenodo.22240815)
- أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
- أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
- النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621341)
هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.
小村 豪 (2026). لماذا نستخدم Generic Host و BackgroundService في تطبيق سطح المكتب. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621341 https://comcomponent.com/ar/blog/2026/03/12/002-generic-host-backgroundservice-desktop-app/
- DOI (أحدث نسخة)
- 10.5281/zenodo.21621341
- DOI (هذه النسخة)
- 10.5281/zenodo.22279737
عندما تنمو أداة Windows أو تطبيق مقيم قليلاً، تزداد بالتدريج المعالجات الواقعة خارج الواجهة.
استطلاع دوريّ، ومراقبة ملفّات، وإعادة اتّصال، ومعالجة طابور، وتهيئة عند البدء، و flush عند الإنهاء.
في البداية يكفي التجاوز بـ Form_Load أو OnStartup أو Task.Run، لكن إن نما الأمر كما هو صار مبهماً من يبدأ ومن يوقف ومن ينظر إلى الاستثناءات.
هذا موضع يفضَّل فيه تقرير من يملك عمر المعالجة قبل طريقة كتابة async / await نفسها.
هنا ينفع Generic Host و BackgroundService في .NET.
جانب UI thread من async / await يتّصل بـ
تنظيم async و UI thread في WPF/WinForms في صفحة واحدة
و
جدول قرار عمليّ لـ C# async/await: Task.Run و ConfigureAwait.
هذه المرّة نضيّق على تنظيم بدء التطبيق كلّه وإيقافه، الطبقة الأبعد من ذلك.
ما يميل إلى التفكّك تدريجيّاً في العمل اليوميّ هو في الغالب هذا النطاق.
- ظهور
Task.Runمن مواضع متفرّقة في النماذج و ViewModel - تبعثر شرط إيقاف الحلقة المقيمة بأعلام
bool - بقاء معالجة تعمل عند الإنهاء، فلا يُغلق التطبيق أحياناً
- صيرورة مداخل السجلات / الإعدادات / DI منفصلة لكلّ تقنيّة
- الرغبة في الإنهاء بـ
Environment.Exit، فيقفزfinally
تفترض هذه المقالة أساساً تطبيقات WPF / WinForms / تطبيقات Windows مقيمة على .NET 6 وما بعده، وتنظّم لماذا ينفع Generic Host / BackgroundService بهدوء، وإلى أيّ حدّ يستحقّ الإدخال، وأين يصير التهاون مؤثّراً لاحقاً.
القارئ المتصوَّر ليس «هل تعرف BackgroundService أم لا» بقدر ما هو من يحار في موضع المعالجة المقيمة وطريقة حمل عمرها. نثبّت المصطلحات من الفصل التالي ليُقرأ حتّى من يرى الاسم أوّل مرّة، ومن يستخدمه أصلاً يمكنه البدء من جدول القرار في 2.2 وتقسيم الفصل 6.
flowchart TB
accTitle: الشكل الذي يتفكّك بالتدريج
accDescr: تبعثر Task.Run، وشروط الإيقاف المتفرّقة بأعلام bool، ومعالجة الإنهاء التي لا تُغلق، والمداخل المنفصلة لكلّ تقنيّة، كلّها تعود إلى نقطة واحدة: إبهام من يبدأ ومن يوقف ومن ينظر إلى الاستثناءات.
s1["Task.Run المتبعثر"] --> core["مالك العمر مبهم"]
s2["شرط إيقاف بأعلام bool"] --> core
s3["معالجة إنهاء لا تُغلق"] --> core
s4["مدخل منفصل لكلّ تقنيّة"] --> core
core --> fix["تقرير من يملك العمر أوّلاً"]
الشكل 1: أشكال التفكّك متعدّدة، والجذر أنّ «من يملك عمر المعالجة» غير مقرَّر.
الشيفرة الواردة في هذه المقالة منشورة على GitHub كمجموعة عيّنات قابلة للبناء والتشغيل (مكتبة، وعرض console يمرّ من التشغيل إلى الإيقاف المنظّم، واختبارات وحدات).
generic-host-backgroundservice-desktop-app - komurasoft-blog-samples (GitHub)
تثبيت المصطلحات أوّلاً
هذا النوع من الحديث يصير صعب القراءة فجأة إن بقيت معاني الكلمات فضفاضة. لذا نثبّت الكلمات المستخدمة في هذه المقالة أوّلاً بإجمال.
- Generic Host
- أساس يجمع «البدء» و«الاعتماديّات» و«الإعدادات» و«السجلات» و«الإيقاف» في تطبيق .NET.
- ليس آليّة خاصّة بـ ASP.NET Core؛ يُستخدم أيضاً في console و worker وتطبيقات سطح المكتب.
- Host /
IHost- الكيان بعد البناء.
- يُشغَّل بـ
StartAsyncويُوقف بـStopAsync.
- Hosted Service
- معالجة مقيمة تُبدأ وتُوقف معلَّقة بعمر الـ host.
- تُكتب بتنفيذ
IHostedService، أو عادةً بالوراثة منBackgroundService.
BackgroundService- تنفيذ مساعد يسهّل كتابة
IHostedService. - يمكن كتابة الجسم طويل العمر في
ExecuteAsync، فيسهل تنظيم حلقات المراقبة والمعالجة الدوريّة.
- تنفيذ مساعد يسهّل كتابة
- lifetime
- في هذه المقالة يعني «متى تبدأ تلك المعالجة، ومتى تنتهي، ومن يملك مسؤوليّة إيقافها».
- أكثر من مدّة البقاء؛ إدارة عمر تشمل مسؤوليّة البدء ومسؤوليّة الإيقاف.
- graceful shutdown
- الإنهاء بعد إرسال إشارة التوقّف وترتيب المعالجة الجارية قدر الإمكان، لا الإنهاء القسريّ.
- يدخل هنا مثلاً «عدم بدء الدورة التالية» و«تقرير إلى أيّ حدّ يُصرَّف الطابور» و«انتظار close أو flush».
- DI
- اختصار Dependency Injection؛ طريقة لاستلام تجميع كائنات الاعتماديّة عبر حاوية بدل كتابتها مباشرةً في جهة الاستدعاء.
- في هذه المقالة يكفي فهم «عدم عمل new للـ logger والإعدادات والـ reader في كلّ موضع، وتكوينها مجتمعة عند المدخل».
هذا الحديث لا يقف عند «تقديم صنف مريح اسمه BackgroundService».
يسهل تتبّعه إن قُرئ بوصفه حديث جمع بدء التطبيق وإيقافه في الـ host، وحمل lifetime المعالجة المقيمة كتصميم.
flowchart TB
accTitle: علاقة الكلمات المستخدمة في هذه المقالة
accDescr: Generic Host أساس البدء والاعتماديّات والإعدادات والسجلات والإيقاف، والكيان بعد البناء هو IHost، وما يُعلَّق بعمره هو Hosted Service، و BackgroundService تنفيذ مساعد يسهّل كتابته.
gh["Generic Host (الأساس)"] --> ihost["IHost (الكيان بعد البناء)"]
ihost --> hs["Hosted Service"]
hs --> bs["BackgroundService"]
bs --> exec["الجسم طويل العمر في ExecuteAsync"]
hs -.-> lt["lifetime = مسؤوليّة البدء والإيقاف"]
الشكل 2: تراتب المصطلحات. Hosted Service يُعلَّق بعمر الـ host، و BackgroundService تنفيذ مساعد له.
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 17، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
المحتويات
- الخلاصة أوّلاً (بجملة)
- التنظيم في صفحة واحدة أوّلاً
- 2.1. الصورة الإجماليّة
- 2.2. جدول قرار الموضع
- لماذا ينفع في تطبيق سطح المكتب
- 3.1. يسهل فصل مسؤوليّة الواجهة عن المعالجة المقيمة
- 3.2. يمكن جمع مداخل البدء والإيقاف والاستثناءات في موضع واحد
- 3.3. يسهل إدخال الإيقاف المنظّم في التصميم
- 3.4. DI / السجلات / الإعدادات تكتمل من البداية
- الحالات التي تلائمه
- مثال تكوين أدنى (مثال WPF)
- تقسيم
StartAsync/ExecuteAsync/StopAsync- 6.1.
StartAsync - 6.2.
ExecuteAsync - 6.3.
StopAsync - 6.4. ملاحظة لـ .NET 10 وما بعده
- 6.1.
- أنماط مضادّة شائعة
- قائمة فحص عند المراجعة
- دليل اختيار تقريبيّ
- الخلاصة
- المراجع
1. الخلاصة أوّلاً (بجملة)
- Generic Host خيار قويّ جدّاً في تطبيق سطح المكتب أيضاً كـ أساس لإدارة البدء و lifetime.
BackgroundServiceوعاء لوضع «المعالجة طويلة العمر» على عمر مُدار بدل إطلاقها بـTask.Runوتركها.- أكثر ما ينفع في العمل اليوميّ هو جمع مسؤوليّة البدء / مسؤوليّة الإيقاف / مراقبة الاستثناءات / السجلات / DI / الإعدادات في تصميم موضع واحد.
- جعل
StartAsyncقصيراً، والجسم طويل العمر فيExecuteAsync، وترتيب الإنهاء فيStopAsyncيحسّن القراءة كثيراً. - التطبيقات المقيمة وتطبيقات الصينيّة ومراقبة الأجهزة والمزامنة الدوريّة والمعالجة اللاحقة المرتبة وحلقات إعادة الاتّصال ملائمة خصوصاً.
- بالمقابل، وضع كلّ معالجة تتحرّك مرّة واحدة عند الضغط على زرّ في
BackgroundServiceيصير مبالغاً فيه قليلاً. StopAsyncمفيد، لكنّه ليس تأميناً ضدّ انهيار العمليّة أو الإنهاء القسريّ. عدم تكديس أعمال الترتيب فيه أكثر من اللازم مهمّ أيضاً.
بمعنى آخر، نفع Generic Host / BackgroundService في تطبيق سطح المكتب ليس لأنّ «ثمّة معالجة خلفيّة»،
بل لأنّكم تريدون حمل عمر تلك المعالجة الخلفيّة كتصميم، لا كملحق للواجهة.
flowchart TB
accTitle: ما يُجمع في تصميم موضع واحد
accDescr: العناصر التي تميل إلى التبعثر — مسؤوليّة البدء ومسؤوليّة الإيقاف ومراقبة الاستثناءات والسجلات و DI والإعدادات — يمكن جمعها في تصميم موضع واحد، وهذا أكثر ما ينفع في العمل اليوميّ.
a["مسؤوليّة البدء"] --> one["الجمع في تصميم موضع واحد"]
b["مسؤوليّة الإيقاف"] --> one
c["مراقبة الاستثناءات"] --> one
d["السجلات و DI والإعدادات"] --> one
one --> win["الوضع على عمر مُدار"]
الشكل 3: قيمة BackgroundService في اجتماع المسؤوليّات المتبعثرة في تصميم موضع واحد.
2. التنظيم في صفحة واحدة أوّلاً
2.1. الصورة الإجماليّة
النظر أوّلاً إلى هذا المخطّط يسرّع الحديث كثيراً.
flowchart LR
A["تشغيل تطبيق سطح المكتب (WPF / WinForms)"] --> B["Build / StartAsync للـ Host"]
B --> C["تجهيز DI / Logging / Configuration"]
B --> D["HostedService.StartAsync"]
D --> E["BackgroundService.ExecuteAsync"]
E --> F["PeriodicTimer / طابور / إعادة اتّصال / حلقة مراقبة"]
C --> G["عرض MainWindow / MainForm"]
F --> H["تحديث الحالة / السجلات / I/O خارجيّ"]
H --> I["الواجهة تستخدم Dispatcher / Invoke في المواضع اللازمة فقط"]
J["إنهاء المستخدم / خطأ قاتل / StopApplication"] --> K["IHost.StopAsync"]
K --> L["إشعار CancellationToken"]
L --> M["HostedService.StopAsync"]
M --> N["close للاتّصال / flush / إيقاف منظّم"]
الشكل 4: الصورة الإجماليّة من تشغيل الـ host إلى عرض الواجهة والحلقة المقيمة وإشارة التوقّف والإيقاف المنظّم.
لمن لا تظهر لديهم الرسوم، نرتّب التدفّق نفسه نصّاً أيضاً.
- تشغيل التطبيق (
App.OnStartupفي WPF،Mainفي WinForms) - تسجيل الخدمات بـ
Host.CreateApplicationBuilderثمّBuild - تشغيل الـ host بـ
IHost.StartAsync(هنا تُثبَّت DI / السجلات / التكوين) - استدعاء
HostedService.StartAsyncللخدمات المسجّلة - انطلاق
BackgroundService.ExecuteAsync(جسم حلقة المراقبة وPeriodicTimerومعالجة الطابور وغيرها) - عرض الواجهة (
MainWindow/MainForm). يحدّث الـ worker مخزن الحالة أو السجلات، وتقرأ الواجهة ذلك في سياقها - عند إنهاء المستخدم أو خطأ قاتل يُستدعى
IHostApplicationLifetime.StopApplication، ويُنتقل إلىIHost.StopAsync - يُشعَر بالإيقاف كـ
CancellationToken(stoppingToken)، فتخرج حلقةExecuteAsync - في
HostedService.StopAsyncيُغلق الاتّصال ويُعمل flush للسجلات ثمّ الإنهاء
الشائع في تطبيقات الواجهة أن تتفرّق المسؤوليّات بالتدريج بين Program.cs / App.xaml.cs / Form_Load / Closing / Task.Run / Timer / static singleton.
بإدخال Host يمكن التقسيم تقريباً كالتالي.
- الواجهة: الشاشة، الإدخال، العرض
- HostedService / BackgroundService: المعالجة المقيمة، المراقبة، معالجة الطابور، المعالجة الدوريّة
- خدمات DI: منطق الأعمال الفعليّ، الاتّصال الخارجيّ، الإعدادات، السجلات
هذا القطع وحده يغيّر سهولة المراجعة كثيراً.
flowchart TB
accTitle: التقسيم الثلاثيّ بعد إدخال الـ host
accDescr: الواجهة للشاشات والإدخال والعرض، و HostedService و BackgroundService للمعالجة المقيمة والمراقبة ومعالجة الطابور والمعالجة الدوريّة، وخدمات DI لمنطق الأعمال والاتّصال الخارجيّ والإعدادات والسجلات.
app["تطبيق سطح المكتب"] --> ui["الواجهة: شاشة وإدخال وعرض"]
app --> hs["HostedService: إقامة ومراقبة"]
app --> di["خدمات DI: منطق الأعمال"]
hs -.-> note["معالجة الطابور والدوريّة هنا أيضاً"]
الشكل 5: من شكل المسؤوليّات المتبعثرة إلى تقسيم ثلاثيّ: الواجهة والمعالجة المقيمة والمعالجة الفعليّة.
2.2. جدول قرار الموضع
| المراد | المرشّح الأوّل للموضع | السبب |
|---|---|---|
| تهيئة خفيفة فور التشغيل | StartAsync |
معناها واضح كمعالجة قصيرة تشارك في البدء |
| مراقبة / استطلاع / إعادة اتّصال طويلة العمر | ExecuteAsync |
يسهل تشغيلها مع عمر الخدمة |
| إشارة التوقّف / flush / close عند الإنهاء | StopAsync |
يسهل كتابة الإيقاف المنظّم مع CancellationToken |
| تكوين الاعتماديّات والإعدادات والسجلات | Host.CreateApplicationBuilder |
يمكن جمع المدخل في موضع واحد |
| تحديث الشاشة | جانب الواجهة | لمس الواجهة مباشرةً من الـ worker أقلّ حوادث |
| معالجة مرّة واحدة لكلّ ضغط زرّ | ميثود async عاديّة |
غالباً لا حاجة إلى HostedService |
| معالجة لاحقة خلفيّة مرتّبة | Channel<T> + BackgroundService |
أسهل في إدارة العمر والحدّ من الإطلاق والترك |
قيمة إدخال Host ليست في «جعل شيء غير متزامن»، بل في وضوح قرار أين يُوضع.
3. لماذا ينفع في تطبيق سطح المكتب
3.1. يسهل فصل مسؤوليّة الواجهة عن المعالجة المقيمة
يبدو تطبيق سطح المكتب كأنّ الواجهة بطله، لكن ما يثقل في العمل اليوميّ هو في الغالب خارج الواجهة.
مثلاً:
- مزامنة حالة كلّ 10 ثوانٍ
- إعادة الاتّصال بجهاز أو خادم
- مراقبة ملفّات والاستيعاب
- معالجة لاحقة مكدّسة في طابور
- نقل سجلات أو إرسال مقاييس
- تسخين الذاكرة المؤقّتة عند التشغيل
هذه ليست «أحداث شاشة» بل معالجات معلَّقة بعمر التطبيق كلّه.
إن أسكنتموها في code-behind للنموذج أو النافذة، تبدأ مسؤوليّة الإيقاف عند إغلاق الشاشة، ومسؤوليّة التقاط الاستثناءات، ومسؤوليّة تقرير إعادة المحاولة أو backoff، بالاختلاط بظروف الواجهة.
باستخدام BackgroundService
يظهر في شكل الشيفرة إعلان أنّ
«هذه المعالجة تعيش طوال عمل التطبيق».
هذا قويّ بهدوء.
flowchart TB
accTitle: مقابلة مواضع إسكان المعالجة المقيمة
accDescr: إسكان المعالجة المقيمة في code-behind يخلط مسؤوليّة الإيقاف والاستثناءات وإعادة المحاولة بظروف الواجهة، أمّا وضعها على BackgroundService فيُخرج إعلان العيش طوال العمل في شكل الشيفرة.
q{"أين تُسكن المعالجة المقيمة"}
q -->|"code-behind"| mix["الإيقاف والاستثناءات وإعادة المحاولة تختلط بالواجهة"]
q -->|"BackgroundService"| decl["إعلان العيش طوال العمل يظهر في الشكل"]
الشكل 6: المعالجة نفسها، وطريقة الإسكان تغيّر اختلاط المسؤوليّات تماماً.
3.2. يمكن جمع مداخل البدء والإيقاف والاستثناءات في موضع واحد
حتّى في تطبيق سطح مكتب بلا Host، يمكن عمل شيء مشابه بترتيب ServiceCollection و ConfigurationBuilder و LoggerFactory كلّ على حدة.
غير أنّ ذلك الشكل يتفرّق بالتدريج في الغالب.
- DI في
Program.cs - الإعدادات static خاصّ
- السجلات factory منفصل
- معالجة الإنهاء في
ApplicationExit - المعالجة المقيمة في
Task.Run
بهذا الشكل يعمل الأمر في البداية. لكن عند المراجعة بعد أشهر يصير من يملك عمر التطبيق صعب الرؤية.
باستخدام Generic Host تدخل في الإطار نفسه:
- تسجيل الخدمات
- قراءة التكوين
- تكوين السجلات
- تشغيل hosted service
- إشعار الإيقاف
- الإيقاف الكلّيّ بـ
IHostApplicationLifetime
أي يسهل جمع مدخل «كيف يبدأ هذا التطبيق وكيف يتوقّف» في موضع واحد. في التطبيقات المقيمة ينفع هذا لاحقاً.
flowchart TB
accTitle: جمع المداخل المتفرّقة في الـ host
accDescr: من شكل تتفرّق فيه DI والإعدادات والسجلات ومعالجة الإنهاء والمعالجة المقيمة في مواضع مختلفة، إلى شكل يدخل فيه تسجيل الخدمات وقراءة التكوين وتكوين السجلات وتشغيل hosted service وإشعار الإيقاف والإيقاف الكلّيّ في الإطار نفسه.
before["المداخل تتفرّق حسب التقنيّة"] --> pain["يصير مالك العمر غير واضح"]
host["الجمع في Generic Host"] --> one["مدخل البدء والإيقاف في موضع واحد"]
one -.-> items["التسجيل والتكوين والسجلات وإشعار الإيقاف"]
الشكل 7: الترتيب المنفصل يعمل أيضاً، لكنّ الدخول في الإطار نفسه هو ما ينفع بعد أشهر.
3.3. يسهل إدخال الإيقاف المنظّم في التصميم
المعالجة المقيمة أصعب في الإيقاف منها في البدء. البدء قد يُكتب في 3 أسطر، أمّا الإنهاء فيزيد ما يُفكَّر فيه دفعة واحدة.
مثلاً عند الإنهاء:
- إلغاء I/O الجاري
- عدم بدء الدورة التالية
- تقرير إلى أيّ حدّ تُصرَّف بقايا الطابور
- إغلاق مقابس أو كائنات COM
- انتظار flush السجلات أو حفظ الحالة
جمع هذا في FormClosing يختلط بظروف الشاشة فيصير شاقاً.
مع Host / BackgroundService يوجد CancellationToken و StopAsync،
فـ مسار التوقّف موجود من البداية.
ليس سحراً طبعاً.
عند الانهيار أو kill قد لا يُستدعى StopAsync.
ومع ذلك، وجود تصميم «عند الإنهاء الطبيعيّ نتوقّف بهذا المسار» وحده يهدّئ الأمر كثيراً.
flowchart TB
accTitle: مسار التوقّف
accDescr: عند الإنهاء يلزم إلغاء I/O الجاري وإيقاف الدورة التالية ومعاملة بقايا الطابور وانتظار close أو flush، ووجود مسار توقّف من البداية عبر CancellationToken و StopAsync هو ما ينفع.
stopreq["إشارة التوقّف"] --> token["إشعار CancellationToken"]
token --> loop["عدم بدء الدورة التالية"]
token --> io["إلغاء I/O الجاري"]
stopreq --> sa["close و flush في StopAsync"]
sa -.-> limit["لا يمرّ عند الانهيار أو kill"]
الشكل 8: المعالجة المقيمة أصعب في الإيقاف، ولهذا ينفع وجود مسار توقّف من البداية.
3.4. DI / السجلات / الإعدادات تكتمل من البداية
حسن Generic Host ليس BackgroundService وحده.
Host.CreateApplicationBuilderيكتمل به أساس DI / التكوين / السجلات- يسهل استخدام
appsettings.jsonومتغيّرات البيئة كما هي - يمكن للواجهة والـ worker استخدام
ILogger<T>بالأسلوب نفسه - يمكن جمع الإعدادات بسلسلة
IOptions<T>عند الحاجة
خصوصاً في مشاريع أدوات Windows، شائع جدّاً أنّ «الإعدادات والـ logger المحمولين بتهاون في static لأنّ المشروع كان صغيراً في البداية يصيران شاقّين لاحقاً».
وضع هذا على الـ host من البداية يقلّل انقطاع النفس عندما يسمن التطبيق قليلاً.
4. الحالات التي تلائمه
Generic Host / BackgroundService ينفعان خصوصاً في حالات كهذه.
- تطبيق مقيم في الصينيّة ثمّة مزامنة دوريّة ومراقبة وإشعارات وإعادة اتّصال
- تطبيق اتّصال أجهزة / كاميرات / مقابس ثمّة إبقاء الاتّصال ومراقبة وإعادة محاولة وجلب حالة
- أداة ربط ملفّات ثمّة مراقبة وطابور استيعاب ومعالجة مرتّبة
- منع تضخّم أداة داخليّة صغيرة في البداية، لكن الإعدادات والسجلات و I/O الخارجيّ مرشّحة للزيادة
- تطبيق جودة إنهائه مهمّة لا يُراد ترك حالة ناقصة عند الإغلاق
بالمقابل، ثمّة حالات لا يلزم إدخال الـ host فوراً.
- أداة صغيرة تُشغَّل مرّة وتعالج مرّة وتنتهي
- شاشة تكتمل بأحداث الواجهة تقريباً بلا معالجة خلفيّة
- أداة مساعدة داخليّة صغيرة حقّاً لا تكاد تزيد فيها الاعتماديّات أو الإعدادات
الـ Host ليس «إلزاماً».
غير أنّه حين تظهر معالجتان مقيمتان أو أكثر، يمكن النظر فيه بإيجابيّة كبيرة.
أرخص بكثير من ترتيب Task.Run المتبعثر لاحقاً.
flowchart TB
accTitle: معيار إدخال الـ host
accDescr: إن ظهرت معالجتان مقيمتان أو أكثر يُنظر في الـ host بإيجابيّة، أمّا الأداة الصغيرة ذات التشغيل الواحد أو الشاشة التي تكتمل بأحداث الواجهة فلا يلزم إدخالها فوراً.
q{"هل تظهر معالجتان مقيمتان أو أكثر"}
q -->|"نعم"| yes["النظر في الـ host بإيجابيّة"]
q -->|"لا"| no["لا يلزم الإدخال فوراً"]
yes -.-> why["أرخص من ترتيب Task.Run لاحقاً"]
الشكل 9: الـ host ليس إلزاماً، لكنّ إدخاله عندما تبدأ المعالجة المقيمة بالزيادة أرخص من الترتيب اللاحق.
5. مثال تكوين أدنى (مثال WPF)
كمثال، نكتب تكويناً أدنى يشغّل host في WPF ويحرّك BackgroundService يقرأ حالة خارجيّة كلّ 5 ثوانٍ.
في WinForms الفكرة شبه نفسها، ويتغيّر المدخل فقط إلى Main / ApplicationContext.
الشيفرة ستظهر في ثلاثة أجزاء، لذا نرتّب تكوين الملفّات أوّلاً.
| الملفّ | المحتوى | الموضع |
|---|---|---|
App.xaml.cs |
توليد الـ host، تسجيل DI، StartAsync / StopAsync، عرض MainWindow |
5.1 |
DevicePollingBackgroundService.cs |
حلقة مقيمة تقرأ الحالة كلّ 5 ثوانٍ | 5.2 |
StatusStore.cs |
حالة يشاركها الـ worker والواجهة. سجلّ DeviceStatus موضوع هنا أيضاً |
5.3 |
IDeviceStatusReader.cs / DeviceStatusReader.cs |
المعالجة التي تقرأ الحالة فعلاً من الخارج | محذوفة من النصّ. التنفيذ في عيّنة GitHub |
MainWindow.xaml / MainWindow.xaml.cs |
الشاشة. تقرأ StatusStore وتعرض |
محذوفة من النصّ. شيفرة شاشة WPF عاديّة |
عيّنة GitHub هي هذا التكوين بصيغة يمكن تشغيلها في console، وتتضمّن تنفيذ BackgroundService و StatusStore، وعرضاً يمرّ من التشغيل إلى الإيقاف المنظّم، واختبارات وحدات.
5.1. App.xaml.cs
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace DesktopHostSample;
public partial class App : Application
{
private IHost? _host;
protected override async void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
HostApplicationBuilder builder = Host.CreateApplicationBuilder(e.Args);
builder.Services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(15);
});
builder.Services.AddSingleton<MainWindow>();
builder.Services.AddSingleton<StatusStore>();
builder.Services.AddScoped<IDeviceStatusReader, DeviceStatusReader>();
builder.Services.AddHostedService<DevicePollingBackgroundService>();
_host = builder.Build();
await _host.StartAsync();
MainWindow mainWindow = _host.Services.GetRequiredService<MainWindow>();
mainWindow.Show();
}
protected override async void OnExit(ExitEventArgs e)
{
if (_host is not null)
{
await _host.StopAsync();
_host.Dispose();
}
base.OnExit(e);
}
}
نقاط هذا الشكل ثلاث.
- تشغيل الـ host قبل عرض الواجهة
- await صريح لـ
StopAsyncعند الإنهاء - جمع DI / hosted service / مهلة الإيقاف عند المدخل
ShutdownTimeout هو الحدّ الأعلى الافتراضيّ الذي ينتظره IHost.StopAsync لمعالجة الإنهاء. القيمة الافتراضيّة تختلف حسب الإصدار: 5 ثوانٍ في .NET 6، و 30 ثانية في .NET 7 وما بعده. كتابة 15 ثانية هنا هي من أجل تقرير الحدّ الأعلى بأنفسكم وفق أبطأ معالجة إنهاء. المعيار تقريباً «مهلة I/O الجاري + الوقت اللازم لـ close / flush» مع هامش صغير. إن قصُر قُطع flush في منتصفه، وإن طال بدا التطبيق «لا يُغلق»، لذا يقلّ الحادث إن قُرّر مرّة بدل ترك الافتراضيّ كما هو.
flowchart TB
accTitle: طريقة تقرير ShutdownTimeout
accDescr: حدّ انتظار معالجة الإنهاء لا يُترك على الافتراضيّ، بل يُقرَّر بأنفسكم وفق أبطأ معالجة إنهاء. إن قصُر قُطع flush في منتصفه، وإن طال بدا التطبيق لا يُغلق.
base["معرفة أبطأ معالجة إنهاء"] --> calc["إضافة حصّة flush إلى مهلة I/O"]
calc --> setv["تقرير الحدّ الأعلى وضبطه"]
setv -.-> short["أقصر من اللازم: يُقطع flush"]
setv -.-> longw["أطول من اللازم: يبدو أنّه لا يُغلق"]
الشكل 10: لا تتركوا ShutdownTimeout على الافتراضيّ؛ احسبوه عكسياً من أبطأ معالجة إنهاء.
جعل OnExit نفسه async يحتاج بعض العناية بسبب إطار الواجهة،
لكن معنى كتابة تدفّق «إيقاف الـ host عند الإنهاء» بوضوح كبير.
5.2. BackgroundService
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace DesktopHostSample;
public sealed class DevicePollingBackgroundService(
IServiceScopeFactory scopeFactory,
StatusStore statusStore,
ILogger<DevicePollingBackgroundService> logger) : BackgroundService
{
public override async Task StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Device polling service is starting.");
await base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Device polling loop started.");
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
using IServiceScope scope = scopeFactory.CreateScope();
IDeviceStatusReader reader =
scope.ServiceProvider.GetRequiredService<IDeviceStatusReader>();
DeviceStatus status = await reader.ReadAsync(stoppingToken);
statusStore.Update(status);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
logger.LogError(ex, "Device polling failed.");
}
}
logger.LogInformation("Device polling loop finished.");
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Device polling service is stopping.");
await base.StopAsync(cancellationToken);
logger.LogInformation("Device polling service stopped.");
}
}
المهمّ هنا كتابة ExecuteAsync
بصراحة كـ حلقة while مُدارة.
- الدورة بـ
PeriodicTimer - الإيقاف بـ
stoppingToken - الاستثناءات بالسجلات
- إن لزمت اعتماديّة
scopedيُقطع scope في كلّ مرّة
بهذا الشكل يصير «أين تبدأ هذه المعالجة المقيمة الآن، وأين تتوقّف، وأين يُرى الفشل» أسهل قراءة بكثير.
flowchart TB
accTitle: شكل حلقة while المُدارة
accDescr: انتظار الدورة بـ PeriodicTimer، وقطع scope في كلّ مرّة لاعتماديّة scoped والقراءة، وتحديث مخزن الحالة، وإبقاء الاستثناءات في السجلّ ومتابعة الحلقة، والخروج بالإلغاء عبر stoppingToken.
tick["انتظار دورة PeriodicTimer"] --> scope["قطع scope وجلب الاعتماديّة"]
scope --> read["قراءة الحالة وتحديث المخزن"]
read --> tick
read -.->|"فشل"| logx["إبقاء الاستثناء في السجلّ والمتابعة"]
tick -.->|"stoppingToken"| exitx["الخروج من الحلقة والإنهاء"]
الشكل 11: ExecuteAsync «حلقة while مُدارة». معاملة الدورة والإيقاف والاستثناءات و scope تُقرأ في موضع واحد.
5.3. لا تجعلوا مشاركة الحالة متّصلة بالواجهة مباشرةً
لمس كائنات الواجهة مباشرةً من الـ worker يعيد مشكلة UI thread في النهاية.
لذا الأأمن أوّلاً:
- الـ worker يحدّث مخزن حالة أو طبقة رسائل
- الواجهة تقرأ تلك الحالة / تطبّقها في سياقها
يمكن جعل StatusStore طبقة مشاركة رقيقة كهذه مثلاً.
namespace DesktopHostSample;
public sealed class StatusStore
{
private readonly object _gate = new();
private DeviceStatus _current = DeviceStatus.Empty;
public DeviceStatus Current
{
get
{
lock (_gate)
{
return _current;
}
}
}
public void Update(DeviceStatus next)
{
lock (_gate)
{
_current = next;
}
}
}
public sealed record DeviceStatus(string Message)
{
public static readonly DeviceStatus Empty = new("No Data");
}
إن لزم إشعار فوريّ للواجهة، استخدموا Dispatcher / BeginInvoke / أحداثاً / messenger وغيرها.
غير أنّ تلك المسؤوليّة أبعد عن الاختلاط إن حُملت عند حدّ الواجهة.
flowchart TB
accTitle: فصل يمنع اتّصال مشاركة الحالة بالواجهة مباشرةً
accDescr: الـ worker لا يلمس كائنات الواجهة مباشرةً بل يحدّث مخزن حالة أو طبقة رسائل، والواجهة تقرأ تلك الحالة في سياقها وتطبّقها. مسؤوليّة الإشعار الفوريّ عند حدّ الواجهة.
worker["worker (الحلقة المقيمة)"] --> store["تحديث مخزن الحالة"]
ui["الواجهة"] --> readq["القراءة في سياقها"]
store --> readq
readq -.-> notify["الإشعار الفوريّ مسؤوليّة حدّ الواجهة"]
الشكل 12: وضع طبقة مشاركة رقيقة بين الـ worker والواجهة يمنع عودة مشكلة UI thread.
6. تقسيم StartAsync / ExecuteAsync / StopAsync
اختلاط هذه الثلاثة يعكّر ذهن القارئ سريعاً. التقسيم التالي مستقرّ إلى حدّ بعيد.
6.1. StartAsync
StartAsync موضع لوضع معالجة قصيرة تشارك في البدء.
ما يلائمه:
- سجلّ التشغيل
- بدء اشتراك خفيف
- تجهيز حالة ابتدائيّة تنتهي سريعاً
- ترتيب أدنى حول
base.StartAsync
ما لا يلائمه:
- تسخين يستغرق عشرات الثواني
- حلقة لا نهائيّة
- جسم معالجة يرتّب I/O ثقيلاً
تثقيل StartAsync يجعل نهوض التطبيق كلّه يبدو بطيئاً.
اعتبار هذا موضع كتابة «إشارة البدء» يقلّل الحوادث.
flowchart TB
accTitle: تمييز ما يُوضع في StartAsync
accDescr: المعالجة القصيرة المشاركة في البدء مثل سجلّ التشغيل وبدء اشتراك خفيف تلائم StartAsync، أمّا التسخين الطويل والحلقة اللانهائيّة و I/O الثقيل فيجعل نهوض التطبيق كلّه يبدو بطيئاً.
q{"هل هي معالجة قصيرة تشارك في البدء"}
q -->|"نعم"| ok["وضعها في StartAsync"]
q -->|"لا"| ng["إلى الجانب الجسميّ مثل ExecuteAsync"]
ng -.-> why["الثقيل يبطئ نهوض التطبيق"]
الشكل 13: StartAsync موضع كتابة «إشارة البدء»، لا موضع المعالجة الثقيلة.
6.2. ExecuteAsync
ExecuteAsync هو جسم عمر الخدمة.
ما يلائمه:
- الاستطلاع
- حلقة المراقبة
- حلقة إعادة الاتّصال
- مستهلك يقرأ
Channel<T> - معالجة دوريّة
- المعالجات التي «تعيش حتى الإيقاف» عموماً
الحيل هنا ثلاث.
- تمرير
CancellationTokenمن البداية إلى النهاية - منع موت الحلقة كلّها بصمت بسبب استثناء
- عدم زيادة إعادة المحاولة أو backoff ارتجالاً أكثر من اللازم
BackgroundService مريح، لكن إن تُرك قد يصير أيضاً «حلقة عملاقة تبتلع كلّ شيء».
اقتطاع المعالجة الفعليّة إلى خدمة أخرى، وجعل ExecuteAsync نفسه أقرب إلى إدارة العمر والتنسيق، أسهل قراءة.
flowchart TB
accTitle: حيل الإبقاء على ExecuteAsync جسماً
accDescr: تمرير CancellationToken من البداية إلى النهاية، ومنع موت الحلقة بصمت بسبب استثناء، وعدم زيادة إعادة المحاولة أو backoff ارتجالاً، واقتطاع المعالجة الفعليّة إلى خدمة أخرى.
exec["ExecuteAsync"] --> c1["تمرير الـ token حتى النهاية"]
exec --> c2["عدم الموت بصمت"]
exec --> c3["عدم زيادة إعادة المحاولة أكثر من اللازم"]
exec -.-> role["التقريب إلى إدارة العمر"]
الشكل 14: ثلاث حيل تُبقي ExecuteAsync موضع إدارة عمر لا حلقة عملاقة.
6.3. StopAsync
StopAsync موضع لـ الترتيب عند الإنهاء الطبيعيّ.
ما يلائمه:
- سجلّ الإيقاف
- إلغاء مؤقّت / اشتراك / مراقبة
- ترتيب موارد تريدون close / flush صريحاً لها
- انتظار الإنهاء عبر
base.StopAsync
غير أنّ عدم انتظار كلّ شيء من StopAsync مهمّ أيضاً.
- سقطت العمليّة
- أُنهي قسريّاً
- قُتل من جانب نظام التشغيل
في إنهاء من هذا النوع قد لا يمرّ أصلاً.
لذا مهمّ:
- إنجاز الاستدامة قدر الإمكان صغيراً في الأوقات العاديّة
- عدم جعل التصميم لا يتّسق إلا عند الإنهاء
- جعل cleanup متماثلاً (idempotent)
محاولة إنقاذ العالم عند الإنهاء وحده تعكّر الأمر في الغالب.
flowchart TB
accTitle: النطاق الذي يجوز انتظاره من StopAsync
accDescr: عند الإنهاء الطبيعيّ يمكن الترتيب في StopAsync، لكنّه قد لا يمرّ عند انهيار العمليّة أو الإنهاء القسريّ أو kill من نظام التشغيل، لذا تُنجَز الاستدامة صغيراً في الأوقات العاديّة ويُجعل cleanup متماثلاً.
endkind{"أيّ نوع إنهاء"}
endkind -->|"إنهاء طبيعيّ"| sa["يمكن الترتيب في StopAsync"]
endkind -->|"انهيار أو kill"| skip["قد لا يمرّ StopAsync"]
skip --> ready["الاستعداد باستدامة الأوقات العاديّة"]
ready -.-> idem["جعل cleanup متماثلاً"]
الشكل 15: StopAsync عون للإنهاء الطبيعيّ، لا تأمين ضدّ الإنهاء غير الطبيعيّ.
6.4. ملاحظة لـ .NET 10 وما بعده
كتغيير كاسر في .NET 10 (إصدار نوفمبر 2025)، تغيّر السلوك بحيث يُنفَّذ BackgroundService.ExecuteAsync كلّه كمهمّة خلفيّة.
سابقاً كان ثمّة سلوك أصعب فهماً قليلاً: الجزء المتزامن قبل أوّل await يحجب بدء الخدمات الأخرى عند التشغيل.
بهذا التغيير قلّ احتمال حادث «الأسطر الأولى من ExecuteAsync كانت تثقل التشغيل».
بالمقابل، إن كان الهدف .NET 9 أو أقدم، فالسلوك ما يزال الجانب الذي لم يتغيّر. تحقّقوا أوّلاً أيّ الجانبين مشروعكم.
ومع ذلك، من جهة التصميم أسهل قراءة الإبقاء على التقسيم:
- معالجة قصيرة تشارك في البدء ←
StartAsync - جسم طويل العمر ←
ExecuteAsync
إن أردتم التحكّم بتوقيت التشغيل بدقّة أكبر، يدخل IHostedLifecycleService في الأفق.
هذا جانب هادئ ينفع عندما يسمن التطبيق المقيم.
flowchart TB
accTitle: فرق سلوك ExecuteAsync حسب الإصدار
accDescr: في .NET 9 وما قبله قد يحجب الجزء المتزامن قبل أوّل await بدء الخدمات الأخرى، ومن .NET 10 يُنفَّذ ExecuteAsync كلّه كمهمّة خلفيّة. في الحالين أسهل قراءة فصل المعالجة القصيرة عند البدء إلى StartAsync.
v{"ما .NET المستهدف؟"}
v -->|".NET 9 وما قبله"| oldb["الجزء المتزامن قبل await قد يسدّ التشغيل"]
v -->|".NET 10 وما بعده"| newb["الكلّ يُنفَّذ في الخلفيّة"]
oldb --> split["المعالجة القصيرة عند البدء إلى StartAsync"]
newb --> split
الشكل 16: السلوك يتغيّر حسب الإصدار، أمّا تصميم فصل StartAsync و ExecuteAsync فلا يتغيّر.
7. أنماط مضادّة شائعة
7.1. بدء حلقة لا نهائيّة في Window_Loaded / Form_Shown
سهل في البداية. لكن مسؤوليّة الإيقاف ومسؤوليّة الاستثناءات تلتصق بجانب الواجهة.
عندما تبدأ شروط مثل «يتوقّف عند إغلاق الشاشة» «لا يتوقّف عند التصغير إلى الصينيّة» «يُعاد التشغيل عند تغيير الإعدادات» بالزيادة، يصير الأمر شاقاً سريعاً.
7.2. إطلاق Task.Run وتركه
Task.Run نفسه ليس سيّئاً.
السيّئ هو غياب مالك للعمر والاستثناءات.
خصوصاً عند بدء معالجة مقيمة بـ Task.Run(async () => { while (...) { ... } }) يصير مبهماً:
- متى تنتهي
- من ينتظر
- كيف تُرى الاستثناءات
- إلى أيّ حدّ يُنتظر عند الإنهاء
وضع هذا على BackgroundService وحده يسهّل التنظيم كثيراً.
7.3. لمس الواجهة مباشرةً من BackgroundService
هذا لغم. مشكلة UI thread ومشكلة lifetime تختلطان دفعة واحدة.
الـ worker لا يعبث بالواجهة مباشرةً، والأأمن وضع حدّ عبر واحد من:
- حالة
- حدث
- رسالة
- طابور
7.4. جمع معالجة الحفظ المهمّة في StopAsync فقط
StopAsync يعين الإنهاء الطبيعيّ، لكنّه ليس الحكم الأخير.
تصميم لا يحفظ إلا عند الإنهاء، ولا يعمل flush إلا عند الإنهاء، ولا يتّسق إلا عند الإنهاء،
ينهار عند الانهيار.
7.5. الإسقاط بتهاون بـ Environment.Exit رغم استخدام الـ host
هذا شائع أيضاً.
استدعاء Environment.Exit بـ
«مزعج، فلنُسقطه»
يقطع بأنفسكم مسار الإيقاف المنظّم الذي يملكه الـ host.
إن أردتم إنهاء الكلّ عند خطأ قاتل،
فالأوضح أوّلاً استخدام IHostApplicationLifetime.StopApplication()
وتمرير المسار النظاميّ للتوقّف.
flowchart TB
accTitle: مسارا الإنهاء الكلّيّ
accDescr: الإسقاط بـ Environment.Exit يقطع مسار الإيقاف المنظّم الذي يملكه الـ host، لذا عند إرادة إنهاء الكلّ بخطأ قاتل يُمرَّر المسار النظاميّ بـ IHostApplicationLifetime.StopApplication.
fatal["إرادة الإنهاء بخطأ قاتل"] --> q{"بأيّهما يُسقَط"}
q -->|"Environment.Exit"| cut["قطع مسار الإيقاف المنظّم"]
q -->|"StopApplication"| route["المسار النظاميّ للتوقّف"]
route --> clean["الإنهاء بعد المرور بـ StopAsync"]
الشكل 17: استخدام Environment.Exit رغم استخدام الـ host يعني قطع مسار التوقّف الذي جهّزتموه بأنفسكم.
8. قائمة فحص عند المراجعة
عند مراجعة تطبيق سطح مكتب يستخدم Generic Host / BackgroundService، النظر بالترتيب التالي أوضح.
- هل تلك المعالجة معلَّقة بعمر التطبيق، أم مجرّد معالجة حدث واجهة
- هل مسؤوليّة التشغيل مقسومة مناسبة بين
StartAsync/ExecuteAsync/StopAsync - هل صار
StartAsyncأثقل من اللازم - هل يمرّر
ExecuteAsyncالـCancellationTokenحتى النهاية - هل لا تُمسَك اعتماديّة
scopedمباشرةً من hosted service - هل لا يلمس الـ worker كائنات الواجهة مباشرةً
- هل لا تُبتلع الاستثناءات بصمت
- هل لم تصر حلقة إعادة المحاولة عالية التكرار بلا حدّ
- هل لوقت الانتظار عند الإنهاء حدّ أعلى
- هل لم يختلط إنهاء يفترض
Environment.Exitأو قتل العمليّة
بهذه القائمة يصير الفرق بين «أدخلنا Host كيفما اتّفق» و «نُظِّم العمر كتصميم» أسهل رؤية بكثير.
9. دليل اختيار تقريبيّ
| المراد | ما يُختار أوّلاً |
|---|---|
| إكمال DI / السجلات / الإعدادات للتطبيق كلّه | Host.CreateApplicationBuilder |
| تشغيل حلقة مقيمة | BackgroundService |
| الدوران بفاصل ثابت | PeriodicTimer + BackgroundService |
| تصريف معالجة لاحقة مرتّبة | Channel<T> + BackgroundService |
| استخدام خدمة scoped | IServiceScopeFactory.CreateScope() |
| إشعار الكلّ بالإنهاء الطبيعيّ | IHostApplicationLifetime.StopApplication() |
| تحديث الواجهة | Dispatcher / Invoke في جانب الواجهة |
| تشغيل شاشة مرّة واحدة | ميثود async عاديّة |
| تحكّم أدقّ بدورة حياة التشغيل | النظر في IHostedLifecycleService |
10. الخلاصة
سبب إدخال Generic Host / BackgroundService إلى تطبيق سطح المكتب
ليس «الرغبة في كتابة تشبه الويب».
ما ينفع حقّاً ثلاثة.
- جمع مسؤوليّة البدء والإيقاف في موضع واحد
- حمل عمر المعالجة طويلة العمر كتصميم
- معاملة الإيقاف المنظّم من المدخل لا كإضافة لاحقة
أدوات Windows والتطبيقات المقيمة تبدأ صغيرة، ثمّ تزيد بالتدريج المراقبة والمزامنة وإعادة الاتّصال والطابور والسجلات والإعدادات. عندئذٍ، التشغيل كملحق لشيفرة الواجهة يصير شاقاً بهدوء لاحقاً.
بالمقابل، الفصل إلى:
- الواجهة واجهة
- المعالجة المقيمة hosted service
- المعالجة الفعليّة خدمة DI
- الإنهاء
StopAsyncوCancellationToken
وحده يرتّب الأمر كثيراً.
flowchart TB
accTitle: تقسيم الخلاصة
accDescr: الفصل إلى الواجهة واجهة، والمعالجة المقيمة hosted service، والمعالجة الفعليّة خدمة DI، والإنهاء StopAsync و CancellationToken، يرتّب الأمر كثيراً.
all["التطبيق كلّه"] --> u["الواجهة واجهة"]
all --> h["المعالجة المقيمة hosted service"]
all --> d["المعالجة الفعليّة خدمة DI"]
all --> s["الإنهاء StopAsync و token"]
الشكل 18: تقسيم بلا بريق، لكنّ ما يقلّل «يصير غريباً أحياناً عند الإغلاق» هو هذا التنظيم.
لا بريق فيه. غير أنّ تصميماً هادئاً كهذا ينفع بقوّة في العمل اليوميّ. يقلّل اللزوجة المزعجة من نوع «يصير غريباً أحياناً عند الإغلاق» و«لا يُعرف أين يُوقَف».
إن كنتم عالقين في أدوات Windows أو تطبيقات مقيمة عند التحويل إلى BackgroundService، أو تصميم البدء / الإيقاف، أو حلقات المراقبة، أو تنظيم عمر مراقبة COM / المقابس / الملفّات، أو فصل عيوب الإنهاء، فاستشيروا من مراجعة التصميم أو ترتيب الاتّجاه.
11. مراجع
- مجموعة شيفرة العيّنة لهذه المقالة (مكتبة، عرض، اختبارات وحدات)، generic-host-backgroundservice-desktop-app
- مدوّنة شركة كومورا سوفت, جدول قرار عمليّ لـ C# async/await: Task.Run و ConfigureAwait
- مدوّنة شركة كومورا سوفت, تنظيم async و UI thread في WPF/WinForms في صفحة واحدة
- Microsoft Learn, .NET Generic Host
- Microsoft Learn, Background tasks with hosted services in ASP.NET Core
- Microsoft Learn, BackgroundService class
- Microsoft Learn, Breaking change: BackgroundService executes all ExecuteAsync as a task
- Microsoft Learn, HostOptions.ShutdownTimeout property
- Microsoft Learn, Logging in C# - .NET
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
CI/CD عمليّ لتطبيقات WinForms / WPF ── أتمتة البناء والتوقيع والتوزيع عبر GitHub Actions
دليل عمليّ لبناء CI/CD لتطبيقات WinForms / WPF عبر GitHub Actions. يشمل YAML الأدنى للبناء والاختبار على windows-latest، وترقيم الإصدار ا...
إقامة تطبيقات Windows في علبة النظام والإشعارات المنبثقة ── مطبات NotifyIcon وكيفية اختيار AppNotification
نرتّب إقامة تطبيقات Windows للأعمال في علبة النظام والإشعارات المنبثقة. نشرح الاستخدام الصحيح لـ NotifyIcon، وإعادة التسجيل عند إعادة تشغ...
انتهاء خدمة برامج تشغيل الطابعات في Windows ── كيف تستعد تطبيقات الأعمال لطباعة التقارير والملصقات
تُنهى Microsoft تدريجيّاً خدمة برامج تشغيل الطابعات v3/v4، ومن تمّوز/يوليو 2026 يُفضَّل برنامج تشغيل فئة IPP. نرتّب في جداول قرار ما يختف...
تعدد اللغات في تطبيقات WinForms/WPF ── الممارسة العملية لملفات resx وتجميعات الأقمار الصناعية وتبديل الثقافة
نرتّب تعدد اللغات في تطبيقات سطح المكتب على Windows: الفرق بين CurrentCulture وCurrentUICulture، وآلية resx وتجميعات الأقمار الصناعية، وا...
الطباعة وإخراج PDF في تطبيقات أعمال Windows ── التمييز بين System.Drawing.Printing وWPF ومكتبات التقارير
نرتّب بجدول قرار حسب المتطلبات طباعة WinForms عبر PrintDocument، وطباعة FlowDocument/FixedDocument في WPF، وخيارات إخراج PDF. ونشرح التحك...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
موضوع قريب من تطوير تطبيقات سطح المكتب نفسه، بما يشمل المعالجة في الخلفيّة والمعالجة الدوريّة وإعادة الاتّصال ومعالجة الإنهاء.
الاستشارات التقنية ومراجعة التصميم
إن أردت أولاً إعادة النظر في تقسيم المسؤوليات بين واجهة المستخدم والمعالجة المقيمة وفي تصميم الإيقاف السلس (graceful shutdown)، فيمكن ترتيب ذلك ضمن الاستشارة التقنية ومراجعة التصميم.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- هل يمكن استخدام Generic Host في تطبيق سطح مكتب أيضاً؟
- نعم. Generic Host ليس آليّة خاصّة بـ ASP.NET Core؛ يمكن استخدامه في تطبيقات console و worker و WPF / WinForms كأساس يجمع البدء والاعتماديّات والإعدادات والسجلات والإيقاف. يلائم خصوصاً التطبيقات المقيمة وتطبيقات الصينيّة ومراقبة الأجهزة والمزامنة الدوريّة والمعالجة اللاحقة المرتبة وحلقات إعادة الاتّصال.
- لماذا نستخدم BackgroundService؟
- وعاء لوضع «المعالجة طويلة العمر» على عمر مُدار بدل إطلاقها بـ Task.Run وتركها. هو تنفيذ مساعد يسهّل كتابة IHostedService، ويمكن كتابة جسم حلقة المراقبة أو المعالجة الدوريّة في ExecuteAsync. يظهر في شكل الشيفرة إعلان أنّ «هذه المعالجة تعيش طوال عمل التطبيق»، فيُجمع مسؤوليّة البدء ومسؤوليّة الإيقاف ومراقبة الاستثناءات والسجلات و DI والإعدادات في تصميم موضع واحد.
- كيف نوزّع StartAsync / ExecuteAsync / StopAsync؟
- يصير أوضح إن وُضعت التهيئة القصيرة المشاركة في البدء في StartAsync، والجسم طويل العمر في ExecuteAsync، وإشارة التوقّف و flush / close عند الإنهاء في StopAsync. في المقابل، معالجة تتحرّك مرّة واحدة عند الضغط على زرّ تكفي لها ميثود async عاديّة؛ وضع كلّ شيء في BackgroundService يصير مبالغاً فيه.
- هل يجوز جمع كلّ معالجة الإنهاء في StopAsync؟
- لا يُستحسن. StopAsync مفيد، لكنّه ليس تأميناً ضدّ انهيار العمليّة أو الإنهاء القسريّ، لذا لا تُكدَّس أعمال الترتيب فيه أكثر من اللازم. يُصمَّم الإيقاف المنظّم (إلغاء I/O الجاري، إيقاف الدورة التالية، سياسة بقايا الطابور، إغلاق الاتّصال، flush السجلات) بـ CancellationToken و StopAsync، مع افتراض منفصل لا ينهار عند الإنهاء غير الطبيعيّ.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.