כשמתחילים לכתוב יישום מסוף או worker ב-.NET, בהתחלה מסתדרים עם קצת קוד ב-Main. אבל ברגע שהיישום גדל מעט, בדרך כלל מצטברים כמה דברים:
- רוצים לקרוא את
appsettings.json - רוצים לדרוס ערכים עם משתני סביבה
- רוצים להוציא לוגים עם
ILogger - לא רוצים שיצירת השירותים תהיה גדושה ב-
new - רוצים להריץ לולאה ברקע
- רוצים להסתיים בצורה מסודרת עם
Ctrl+Cאו עצירת שירות
כאן נכנס לתמונה Generic Host. אבל גם השם הזה נוטה להתבלבל מעט.
- מה ההבדל בין
Host.CreateApplicationBuilderל-Host.CreateDefaultBuilder - האם
IHostזהה למכל DI - מה הקשר ל-
BackgroundService - האם זה משהו נפרד מ-
WebApplicationBuilderשל ASP.NET Core - האם יש ערך בשימוש בו גם ביישום מסוף
כשהדברים האלה מתערבבים, Generic Host נראה לפעמים כ”משהו ייעודי ליישומי Web”, ולפעמים כ”משהו שצריך להפוך אליו כל דבר”. שתי התפיסות קצת רשלניות.
במאמר הזה, בהנחת התחושה המעשית הנוכחית שמתמקדת בעיקר ב-.NET 6 ואילך, נסדר קודם ארבעה דברים:
- מהותו של Generic Host
- מה הוא מטפל בו יחד
- הקשר בין
Host.CreateApplicationBuilder, Host.CreateDefaultBuilderו-WebApplication.CreateBuilder - מאיפה נעים להתחיל
תוכן עניינים
- קודם המסקנה (במשפט אחד)
- 1.1. קודם קובעים מינוח
- הטבלה שכדאי לראות ראשונה
- 2.1. מה Generic Host מחזיק
- 2.2. ההבדל בין ה-builder-ים
- 2.3. למה יש כמה נקודות כניסה
- התמונה הכוללת של Generic Host (תרשים)
- מה טוב ב-Generic Host
- 4.1. אפשר לרכז את קוד ההפעלה במקום אחד
- 4.2. DI / תצורה / לוגים מתחברים מההתחלה
- 4.3. קל לטפל בסיום תקין ובהרצה קבועה
- הרכב מינימלי
- 5.1. דוגמה מינימלית ליישום מסוף
- 5.2.
appsettings.json - 5.3. הוספת
BackgroundService
- תבניות טיפוסיות
- 6.1. כלי מסוף קצר-חיים
- 6.2. worker / שירות רקע
- 6.3. גם מתחת ל-ASP.NET Core
- מקרים שמתאימים
- מקרים שלא מתאימים / עודפים
- מוקשים נפוצים
- סיכום
- מקורות
מפת הידע של המאמר
.NET Generic Host הוא מנגנון המאחד לתוך תשתית אחת הזרקת תלויות, תצורה, לוגים, ותהליך רקע קבוע באמצעות IHostedService או BackgroundService, וכן ניהול מחזור חיים שמגיב ל-Ctrl+C או ל-SIGTERM. ביישום לא-Web חדש מקובל לבנות אותו דרך Host.CreateApplicationBuilder, ולקוד קיים נשאר גם המסלול Host.CreateDefaultBuilder. WebApplication.CreateBuilder של ASP.NET Core הוא הכניסה שמרחיבה את אותה גישה לעולם ה-Web, במקום ה-Web Host הנפרד שהיה קיים בעבר. ל-BackgroundService אין scope כברירת מחדל, ולכן שימוש בשירותים מסוג scoped מחייב יצירת scope במפורש דרך IServiceScopeFactory — וזו נקודת המכשול הנפוצה ביותר.
flowchart LR
accTitle: מפת הידע של .NET Generic Host
accDescr: תרשים המראה כיצד Generic Host מאחד DI, תצורה, לוגים, IHostedService ו-BackgroundService וניהול מחזור חיים, וכיצד הוא מתחבר ל-Host.CreateApplicationBuilder ול-WebApplication.CreateBuilder.
generic_host["Generic Host"]
dependency_injection_dotnet["הזרקת תלויות (DI)"]
dotnet_configuration["מערכת התצורה של .NET (IConfiguration)"]
dotnet_ilogger["Microsoft.Extensions.Logging(ILogger)"]
ihostedservice["IHostedService"]
host_lifetime["ניהול אורך החיים של ה-Host (IHostApplicationLifetime)"]
backgroundservice["BackgroundService"]
host_create_application_builder["Host.CreateApplicationBuilder"]
host_create_default_builder["Host.CreateDefaultBuilder"]
web_application_builder["WebApplication.CreateBuilder"]
web_host_legacy["Web Host(IWebHostBuilder)"]
iservice_scope_factory["IServiceScopeFactory"]
dotnet_options_pattern["דפוס האפשרויות (Options)"]
windows_service["שירות Windows"]
dotnet[".NET (מ-Core ואילך)"]
host_application_builder["HostApplicationBuilder"]
generic_host -->|"משתמש ב"| dependency_injection_dotnet
generic_host -->|"משתמש ב"| dotnet_configuration
generic_host -->|"משתמש ב"| dotnet_ilogger
generic_host -->|"משתמש ב"| ihostedservice
generic_host -->|"משתמש ב"| host_lifetime
backgroundservice -->|"מממש את"| ihostedservice
generic_host -->|"מוגדר באמצעות"| host_create_application_builder
generic_host -->|"מוגדר באמצעות"| host_create_default_builder
web_application_builder -->|"יורש את"| web_host_legacy
web_application_builder -->|"משתמש ב"| generic_host
backgroundservice -.->|"מחייב"| iservice_scope_factory
dotnet_options_pattern -->|"משתמש ב"| dotnet_configuration
generic_host -.->|"משתמש ב"| windows_service
ihostedservice -.->|"מחייב"| host_lifetime
generic_host -->|"מחייב"| dotnet
host_create_application_builder -->|"משתמש ב"| host_application_builder
בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 16, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle
1. קודם המסקנה (במשפט אחד)
- Generic Host הוא תשתית שמטפלת יחד בהפעלה ובאורך החיים של יישום .NET.
- בתוכה נכללים DI, תצורה, לוגים,
IHostedService/BackgroundService, וטיפול בעצירת היישום. - ביישום חדש שאינו Web, טבעי להתחיל מ-
Host.CreateApplicationBuilder(args). - גם
WebApplicationBuilderשל ASP.NET Core אינו עולם נפרד — הוא שער שמרחיב את אותה תפיסת host לצרכי Web. - כלומר Generic Host אינו סיפור של מכל DI לבדו, אלא מנגנון שמאחד את נקודת ההרכבה של היישום עם ניהול אורך החיים שלו.
בקיצור, מרגע שהיישום עובר קצת מעבר ל”קורא ארגומנטים, מציג פעם אחת ומסתיים”, Generic Host מתחיל להשפיע משמעותית. מהצד השני, לא חייבים להביא אותו בכל פעם גם לכלי קטן שעוד לא הגיע לשלב הזה.
flowchart TB
accTitle: הטווח שבו Generic Host מתחיל להשפיע
accDescr: תרשים המראה שכלי קטן שרק מציג פעם אחת ומסתיים לא צריך להביא בכל פעם, ואילו יישום שעבר מעבר לכך נהנה מהשפעה משמעותית של Generic Host.
small1["כלי שמציג פעם אחת ומסתיים"] -.->|"לא מביאים בכל פעם"| ghost0["Generic Host"]
grown1["יישום שגדל מעבר לכך"] -->|"משפיע משמעותית"| ghost0
איור 1: מרגע שהיישום עובר קצת מעבר ל”מציג פעם אחת ומסתיים”, Generic Host מתחיל להשפיע.
1.1. קודם קובעים מינוח
המאמר הזה ישתמש בהמשך הרבה במטפורות, אז קודם נניח כאן ניסוח מדויק.
| מונח | הניסוח המדויק | המטפורה שנשתמש בה במאמר |
|---|---|---|
| DI (הזרקת תלויות / Dependency Injection) | דרך בנייה שבה מחלקה לא יוצרת בעצמה עם new את הצדדים שהיא צריכה, אלא מקבלת אותם מבחוץ. מקום האחסון שבו רושמים מראש את הצדדים שיימסרו הוא מכל ה-DI (IServiceProvider), ול-Generic Host יש אותו מההתחלה |
חיווט |
Builder (HostApplicationBuilder) |
אובייקט להרכבת ה-host. יש לו מאפיינים כמו Services, Configuration ו-Logging, ואליהם רושמים דברים. עד קריאה ל-Build() היישום לא רץ |
שולחן הרכבה |
Host (IHost) |
גוף היישום המורכב שמתקבל כתוצאה מ-Build(). הוא מחזיק את מכל ה-DI, את התצורה, את הלוגים ואת ה-hosted service-ים, ומטפל בהם מ-Run() / RunAsync() ועד לעצירה |
תשתית |
Hosted service (IHostedService / BackgroundService) |
מיכל לתהליך שרץ בהתאם להפעלה ולעצירה של ה-host. כשה-host עולה, נקרא StartAsync, וב-BackgroundService רץ ExecuteAsync |
עבודת רקע קבועה |
| Lifetime | ניהול מההפעלה ועד לעצירה של היישום. בתגובה לאותות כמו Ctrl+C, SIGTERM או עצירת שירות, מסדירים את דרך הסיום |
אורך חיים |
הבלבול הנפוץ ביותר הוא בין Builder ל-Host. ה-Builder הוא הצד המרכיב, וה-Host הוא התוצאה המורכבת, ו-Build() הוא הגבול ביניהם. ברגע שזה ברור, גם הניסוחים “תשתית”, “קופסה”, “שער” ו”נקודת כניסה” שיופיעו בהמשך יהיו קלים להבנה בלי בלבול לגבי מה הם מתכוונים אליו.
flowchart TB
accTitle: הגבול בין Builder ל-Host
accDescr: תרשים המראה ש-Builder הוא הצד המרכיב ו-IHost הוא התוצאה המורכבת, וקריאת Build היא הגבול ביניהם.
bld1["Builder (הצד המרכיב)"] -->|"Build()"| hst1["IHost (התוצאה המורכבת)"]
hst1 --> life1["Run / RunAsync מהפעלה עד עצירה"]
איור 2: Builder הוא הצד המרכיב ו-IHost הוא התוצאה המורכבת, ו-Build() הוא הגבול ביניהם.
אם DI חדש לכם, כדאי לחשוב כך: במקום לכתוב בעצמכם שרשרת של new, רושמים בזמן ההפעלה “כשיהיה צורך בסוג הזה, תמסרו את המימוש הזה”, והצד המקבל רק מקבל אותו כארגומנט בבנאי. מקום הרישום הזה הוא builder.Services.
2. הטבלה שכדאי לראות ראשונה
2.1. מה Generic Host מחזיק
קודם כל, כדאי מאוד לפרק את תוכן הקופסה הזו.
| רכיב | מה Generic Host מטפל בו | מה יוצא מזה |
|---|---|---|
| DI | מרכיב שירותים מתוך IServiceCollection |
קל יותר לצמצם שרשרת של new |
| Configuration | מאחד appsettings.json, משתני סביבה וארגומנטים משורת הפקודה |
קל יותר להתמודד עם הבדלים בין סביבות |
| Logging | בונה תשתית לשימוש ב-ILogger<T> |
קל יותר להחליף אחר כך את יעד פלט הלוגים |
| Hosted service | מטפל בהפעלה ובעצירה של IHostedService / BackgroundService |
קל יותר להפריד תהליך רקע מגוף היישום |
| Lifetime | מטפל בהפעלה ובעצירה דרך IHostApplicationLifetime, IHostEnvironment וכדומה |
קל יותר להסדיר את דרך הסיום עם Ctrl+C, SIGTERM או עצירת שירות |
חשוב כאן להבין ש-Generic Host אינו “עטיפת DI נוחה אחת בלבד”. בפועל, ההשקפה שהכי לא מטעה היא: קופסה שמחווטת יחד את כל סביבת נקודת הכניסה של היישום.
2.2. ההבדל בין ה-builder-ים
גם כאן מהיר יותר לראות הכול על טבלה אחת מראש.
| נקודת כניסה | שימוש עיקרי | סגנון כתיבה | הבחירה הראשונה |
|---|---|---|---|
Host.CreateApplicationBuilder(args) |
יישום חדש שאינו Web — מסוף / worker וכדומה | כותבים ישירות אל builder.Services / builder.Configuration / builder.Logging |
ליישום חדש — זה |
Host.CreateDefaultBuilder(args) |
קוד קיים או הרכבה שמבוססת בעיקר על שיטות הרחבה ישנות | משרשרים קריאות כמו ConfigureServices |
אם יש נכסים קיימים — זה |
WebApplication.CreateBuilder(args) |
יישום Web / API של ASP.NET Core | נקודת כניסה שמוסיפה ל-Generic Host נוחות ייעודית ל-Web | ליישום Web — זה |
CreateApplicationBuilder ו-CreateDefaultBuilder אינם מקרה שבו אחד תכונה חדשה והשני משהו נפרד.
לשניהם אותה פונקציונליות ליבה ואותה התנהגות ברירת מחדל. ההבדל העיקרי הוא סגנון הכתיבה.
ביישום חדש שאינו Web, כיום טבעי להתחיל מ-Host.CreateApplicationBuilder(args). כדאי לחשוב על WebApplication.CreateBuilder(args) כעל אותו זרם, מורחב לצרכי Web.
flowchart TB
accTitle: הקשר בין שלוש נקודות הכניסה
accDescr: תרשים המראה ש-CreateApplicationBuilder ו-CreateDefaultBuilder חולקים אותה פונקציונליות ליבה ואותה התנהגות ברירת מחדל אך נבדלים בסגנון הכתיבה, ו-WebApplication.CreateBuilder הוא נקודת כניסה שמרחיבה את אותו זרם לצרכי Web.
appb1["CreateApplicationBuilder"] --> core1["אותה פונקציונליות ליבה והתנהגות ברירת מחדל"]
defb1["CreateDefaultBuilder"] --> core1
appb1 -.-> sty1["סגנון כתיבה ישיר"]
defb1 -.-> sty2["סגנון שרשור"]
core1 -.->|"נקודת כניסה מורחבת ל-Web"| webb1["WebApplication.CreateBuilder"]
איור 3: שני ה-builder-ים חולקים אותה פונקציונליות ליבה ואותה התנהגות ברירת מחדל ונבדלים רק בסגנון הכתיבה, ולצורכי Web יש נקודת כניסה שמרחיבה את אותו זרם.
2.3. למה יש כמה נקודות כניסה
יש כמה נקודות כניסה מכיוון שהצד של Web והצד שאינו Web גדלו בנפרד ואז התמזגו.
- במקור, ל-ASP.NET Core היה Web Host ייעודי ל-Web (
IWebHostBuilder), ו-Generic Host (IHostBuilder) ליישומים שאינם Web הוכן בנפרד. - לאחר מכן ASP.NET Core עבר להתבסס על Generic Host, כך שגם Web וגם לא-Web שוכנים על אותה תפיסת host.
- בנוסף, לצד סגנון הכתיבה שמחבר callback-ים בשרשרת (
ConfigureServicesוכדומה), התווספה נקודת כניסה של כתיבה ישירה אל מאפיינים (builder.Servicesוכדומה).Host.CreateApplicationBuilderו-WebApplication.CreateBuilderשייכים לצד הזה.
בתיעוד הרשמי הנוכחי, קבוצת Host.CreateApplicationBuilder (IHostApplicationBuilder) מוגדרת כמיועדת לפרויקטים חדשים, וברירת המחדל של התבניות הנוכחיות, וקבוצת Host.CreateDefaultBuilder (IHostBuilder) מוגדרת כהדרך המסורתית שנשארת לצורך תאימות עם קוד קיים. גם ההסבר ששניהם חולקים אותה פונקציונליות ליבה ואותה התנהגות ברירת מחדל כתוב במפורש.
אם מגיעים מקוד מתקופת .NET Framework או .NET Core 3.1, יש תחושה של “למה יש שתי דרכי כתיבה”. אבל קל יותר להשלים עם זה כשמבינים שזה לא מדובר בשני דברים נפרדים חדש-ישן, אלא בנקודות כניסה שהתרבו בתהליך ההתמזגות. אם אין סיבה להתאים לנכסים קיימים, אפשר בהחלט להשתמש ב-Host.CreateApplicationBuilder ביישום חדש.
flowchart TB
accTitle: הרקע לריבוי נקודות הכניסה
accDescr: תרשים המראה ש-Web Host הייעודי ל-Web ו-Generic Host ליישומים שאינם Web היו נפרדים, ש-ASP.NET Core התמזג לתוך Generic Host, ושבנוסף התווספה נקודת כניסה של כתיבה ישירה אל מאפיינים.
wh1["Web Host (IWebHostBuilder)"] --> mg1["ASP.NET Core מתמזג עם Generic Host"]
gh1["Generic Host (IHostBuilder)"] --> mg1
mg1 --> ad1["נוספת נקודת כניסה של כתיבה ישירה"]
ad1 -.-> ex1["CreateApplicationBuilder ו-WebApplication.CreateBuilder"]
איור 4: Web Host ו-Generic Host שגדלו בנפרד התמזגו, ובתהליך הזה התרבו נקודות כניסה בסגנון הכתיבה הישיר.
3. התמונה הכוללת של Generic Host (תרשים)
בקווים כלליים, התמונה הכוללת נראית כך:
flowchart LR
accTitle: התמונה הכוללת של Generic Host
accDescr: תרשים המראה כיצד ארגומנטים, משתני סביבה ו-appsettings.json זורמים אל ה-builder, איך רושמים תצורה, שירותים ולוגים, ואיך Build מוביל ל-IHost שמופעל דרך Run או RunAsync ומחובר ל-lifetime ול-hosted service.
args1["args / משתני סביבה / appsettings.json"] --> builder1["Host.CreateApplicationBuilder(args)"]
builder1 --> config1["builder.Configuration"]
builder1 --> services1["builder.Services"]
builder1 --> logging1["builder.Logging"]
services1 --> hosted1["IHostedService / BackgroundService"]
builder1 --> build1["builder.Build()"]
build1 --> host1["IHost"]
host1 --> run1["Run / RunAsync"]
run1 --> lifetime1["הפעלה · עצירה · Ctrl+C · SIGTERM"]
lifetime1 --> hosted1
איור 5: רושמים תצורה, שירותים ולוגים אל ה-builder, מריצים את ה-IHost שמתקבל מ-Build() דרך Run/RunAsync, וה-lifetime מתחבר עד להפעלה ולעצירה של ה-hosted service.
בדרך כלל יוצרים את ה-builder ב-Program.cs, מוסיפים שירותים אל builder.Services, מכווננים לפי הצורך את builder.Configuration ואת builder.Logging, ולבסוף קוראים ל-Build() כדי לקבל IHost ומריצים אותו עם Run() / RunAsync().
מה שקצת חבוי אבל משמעותי הוא שכבר בשלב Host.CreateApplicationBuilder(args) הרבה דברים כבר טעונים. כברירת מחדל נכללים למשל אלה:
- שורש התוכן הוא הספרייה הנוכחית
- תצורת ה-host היא משתני סביבה עם הקידומת
DOTNET_וארגומנטים משורת הפקודה - תצורת היישום היא
appsettings.json,appsettings.{Environment}.json, סודות משתמש (user secrets) בסביבת Development, משתני סביבה וארגומנטים משורת הפקודה - הלוגים הם Console / Debug / EventSource / EventLog (Windows בלבד)
- בסביבת
Developmentיש אימות scope ואימות תלויות
כלומר לא מחווטים מאפס בלי לחשוב, אלא מונחת מההתחלה “תשתית שמספיקה יפה לשימוש רגיל”.
flowchart TB
accTitle: ברירות המחדל שכבר טעונות מההתחלה
accDescr: תרשים המראה שכבר בשלב CreateApplicationBuilder טעונות תצורת host, תצורת יישום ולוגים בברירת מחדל, ושתשתית שמספיקה לשימוש רגיל מונחת מההתחלה.
ent1["CreateApplicationBuilder(args)"] --> dz1["תצורת host (משתני DOTNET_ וארגומנטים)"]
ent1 --> dz2["תצורת יישום (appsettings.json ועוד)"]
ent1 --> dz3["לוגים בברירת מחדל (Console ועוד)"]
dz1 --> rdy1["תשתית שמספיקה לשימוש רגיל"]
dz2 --> rdy1
dz3 --> rdy1
איור 6: כבר בשלב יצירת ה-builder נטענות ברירות מחדל של תצורה ולוגים, ולא מחווטים הכול מאפס.
4. מה טוב ב-Generic Host
4.1. אפשר לרכז את קוד ההפעלה במקום אחד
ההשפעה הכי שקטה והכי גדולה של Generic Host היא שקוד ההפעלה של היישום פחות נוטה להתפזר.
כשיישום גדל מעט, אלה הדברים שמצטברים סביב Main:
- טעינת קבצי תצורה
- החלפה בין סביבות
- אתחול הלוגר
- הרכבת
HttpClient, repository או service - הפעלת תהליך רקע
- ניקוי בעת אות סיום
אם מחברים את כל זה ידנית בלי host, בהתחלה זה קל, אבל בהמשך נקודת הכניסה נהיית דביקה יותר ויותר.
כשמשתמשים ב-Generic Host, Program.cs נהיה בבירור “המקום שבו מרכיבים יחד את התלויות”. רק הסידור הזה משנה משמעותית עד כמה קל לעשות code review.
flowchart TB
accTitle: קוד ההפעלה מתרכז במקום אחד
accDescr: תרשים המראה שחיווט ידני בלי host גורם לנקודת הכניסה להיות דביקה בהמשך, ואילו שימוש ב-Generic Host הופך את Program.cs למקום ברור שבו מרכיבים את התלויות.
no1["חיווט ידני בלי host"] --> st1["נקודת הכניסה נהיית דביקה בהמשך"]
yes1["שימוש ב-Generic Host"] --> pg1["Program.cs נהיה מקום הרכבה"]
pg1 -.-> rv1["קל יותר לעשות code review"]
איור 7: בחיווט ידני נקודת הכניסה נעשית דביקה בהמשך, אבל כשמרכזים אל ה-host, Program.cs נהיה בבירור מקום ההרכבה.
4.2. DI / תצורה / לוגים מתחברים מההתחלה
כשמשתמשים ב-Generic Host, DI, תצורה ולוגים יושבים מההתחלה על אותה תשתית.
לדוגמה, בצד המחלקה אפשר לקבל בפשטות דברים כמו אלה:
ILogger<T>IConfigurationIHostEnvironmentIOptions<T>
מה שמשמעותי כאן הוא שדרך קריאת התצורה ודרך בניית השירותים לא נוטות להיות שני סגנונות נפרדים.
אם יש הגדרה אחת או שתיים, אפשר להסתדר גם עם קריאה ישירה של IConfiguration["Section:Key"]. אבל כשההגדרות גדלות בעבודה מעשית, בטוח יותר לאגד אותן לפי section למחלקה עם IOptions<T>. קו הגבול הוא בערך כשמספר מחרוזות המפתח עובר 5. בהיקף הזה, טעויות הקלדה מתחילות להופיע ככשלים שלא מתגלים עד זמן הריצה, וגם קשה יותר לעקוב היכן נקרא כל מפתח.
באופן דומה, גם בלוגים, במקום לבנות ידנית ILoggerFactory בכל מקום, עדיף להזריק ILogger<T> למחלקה הרלוונטית — כך קל יותר לעקוב.
מה ש-Generic Host נוח בו הוא שהוא לא הופך את הדברים האלה לנושאים נפרדים, אלא מאפשר לטפל בהם יחד כתשתית אחת של כל היישום.
flowchart TB
accTitle: איך מתפתחת דרך קריאת התצורה
accDescr: תרשים המראה שאם יש הגדרה אחת או שתיים אפשר להסתדר עם קריאה ישירה של IConfiguration, אך משמספר מחרוזות המפתח עובר 5 בטוח יותר לאגד אותן לפי section עם IOptions.
few1["הגדרה אחת או שתיים"] --> dr1["קריאה ישירה של IConfiguration"]
many1["יותר מ-5 מחרוזות מפתח"] --> op1["איגוד למחלקה עם IOptions"]
many1 -.-> rk1["טעויות הקלדה לא מתגלות עד זמן הריצה"]
איור 8: כשההגדרות מעטות מספיקה קריאה ישירה, אבל משמספר המפתחות עובר 5 בטוח יותר לאגד אותן עם IOptions.
4.3. קל לטפל בסיום תקין ובהרצה קבועה
Generic Host מטפל לא רק ב”איך מפעילים” אלא גם ב”איך עוצרים”.
כשה-host עולה, נקרא StartAsync של כל IHostedService שנרשם. בשירות worker, רץ ExecuteAsync של ה-hosted service, כולל BackgroundService.
“סיום תקין” כאן פירושו לא לחתוך את התהליך בפתאומיות, אלא לסיים לפי הסדר הבא:
- לשדר את אות העצירה
- לצאת מהלולאה או מההמתנה
- לנקות חיבורים ומשאבים
ביישום שרץ זמן רב, זה קריטי. אירועים כמו Ctrl+C, SIGTERM או עצירת שירות מאפשרים להסדיר את דרך העצירה של כל היישום.
בנוסף, כשרוצים לבקש סיום מצד היישום עצמו, אפשר להשתמש ב-IHostApplicationLifetime.StopApplication(). אפשר לשדר בהקשר ה-host את האות “העבודה הסתיימה, אפשר לרדת בצורה מסודרת”.
flowchart TB
accTitle: סדר הסיום התקין
accDescr: תרשים המראה שבתגובה לאירוע Ctrl+C, SIGTERM או עצירת שירות, משדרים אות עצירה, יוצאים מהלולאה או מההמתנה, ואז מנקים חיבורים ומשאבים.
sg1["Ctrl+C / SIGTERM / עצירת שירות"] --> p1["שידור אות עצירה"]
p1 --> p2["יציאה מלולאה או המתנה"]
p2 --> p3["ניקוי חיבורים ומשאבים"]
ap1["בקשת סיום מצד היישום"] -.->|"StopApplication()"| p1
איור 9: סיום תקין עובר לפי הסדר של אות עצירה, יציאה מלולאה וניקוי, וגם מצד היישום אפשר לשדר את אותו אות דרך StopApplication().
5. הרכב מינימלי
5.1. דוגמה מינימלית ליישום מסוף
חשוב קודם: השימוש ב-Generic Host לא מחייב ליצור דווקא BackgroundService.
גם בכלי מסוף שרץ פעם אחת, אם רוצים DI, תצורה ולוגים, Generic Host שימושי לגמרי.
אם מוסיפים אותו בדיעבד לפרויקט console רגיל, קודם מוסיפים הפניה ל-Microsoft.Extensions.Hosting.
dotnet add package Microsoft.Extensions.Hosting
הדוגמה המינימלית ל-Program.cs יכולה להיראות כך:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<JobRunner>();
using IHost host = builder.Build();
try
{
JobRunner runner = host.Services.GetRequiredService<JobRunner>();
await runner.RunAsync();
return 0;
}
catch (Exception ex)
{
ILogger logger = host.Services
.GetRequiredService<ILoggerFactory>()
.CreateLogger("Program");
logger.LogError(ex, "Unhandled exception occurred during job execution.");
return 1;
}
internal sealed class JobRunner(
ILogger<JobRunner> logger,
IConfiguration configuration,
IHostEnvironment hostEnvironment)
{
public Task RunAsync()
{
string message = configuration["Sample:Message"] ?? "(no message)";
logger.LogInformation("Environment: {EnvironmentName}", hostEnvironment.EnvironmentName);
logger.LogInformation("Message: {Message}", message);
return Task.CompletedTask;
}
}
בהרצת dotnet run, המסוף מציג כך (הערך של Message מגיע מ-appsettings.json שנניח בסעיף 5.2 הבא):
info: JobRunner[0]
Environment: Production
info: JobRunner[0]
Message: hello from Generic Host
מה שמופיע מימין ל-info: הוא קטגוריית הלוג (כאן שם הסוג, כי מדובר ב-ILogger<JobRunner>) ומזהה האירוע. הלוגר בברירת המחדל של הקונסולה מציג בצורה של “שורה ראשונה — קטגוריה, שורה שנייה — גוף ההודעה”. הסיבה ש-Environment הוא Production היא שזו ברירת המחדל כשלא הוגדרו לא DOTNET_ENVIRONMENT ולא ASPNETCORE_ENVIRONMENT. כדי להחליף בזמן פיתוח, מריצים עם DOTNET_ENVIRONMENT=Development מוגדר.
אם לא צריך הרצה קבועה לאורך זמן, לא חייבים להגיע עד RunAsync(). אפשר לעשות Build(), לפתור את השירות הנדרש, ולסיים בתום העבודה. גם כך מקבלים במלואו את היתרון של Generic Host.
זו נקודה חשובה יותר ממה שנראה. לא חייבים להביא בכל פעם את תבנית ה-Worker לעבודות קצרות-חיים.
flowchart TB
accTitle: איך משתמשים בעבודה קצרת-חיים
accDescr: תרשים המראה שאם לא צריך הרצה קבועה, לא חייבים להגיע עד RunAsync, אלא אפשר לעשות Build, לפתור את השירות הנדרש, לבצע את העבודה ולסיים ישירות — וגם כך מקבלים את היתרון של Generic Host.
st2["ביצוע Build()"] --> rs1["פתרון השירות הנדרש"]
rs1 --> jb1["ביצוע העבודה"]
jb1 --> fin1["סיום ישיר"]
fin1 -.-> nt1["לא חייבים להגיע עד RunAsync()"]
איור 10: בעבודה קצרת-חיים, גם עם Build(), פתרון שירות, ביצוע וסיום — כבר מקבלים את היתרון של ה-host.
5.2. appsettings.json
בדוגמה שלמעלה, מספיקה צורה מינימלית כזו לקובץ התצורה:
{
"Sample": {
"Message": "hello from Generic Host"
}
}
יש מוקש קלאסי אחד. בפרויקט console, רק הוספת appsettings.json אינה גורמת להעתקה לתיקיית הפלט. או שמכוונים במאפייני הפרויקט את “העתק לתיקיית הפלט” ל”העתק אם חדש יותר”, או שכותבים ב-csproj את הבא:
<ItemGroup>
<Content Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
כדאי גם להכיר את התסמין כשזה נשכח. Generic Host קורא את appsettings.json כקובץ אופציונלי, כך שגם אם הוא לא קיים, לא נזרק חריג. פשוט לא מתקבל ערך. בדוגמה המינימלית שלמעלה, יוצג Message: (no message). כשיש “אין שגיאה אבל התצורה לא עובדת”, קודם בדקו האם appsettings.json נמצא בתיקיית הפלט.
flowchart TB
accTitle: התסמין כש-appsettings.json חסר
accDescr: תרשים המראה שאם appsettings.json לא הועתק לתיקיית הפלט, הוא עדיין נקרא כקובץ אופציונלי, ולכן לא נזרק חריג, ופשוט לא מתקבל ערך.
ms1["הקובץ חסר בתיקיית הפלט"] --> rd1["נקרא כקובץ אופציונלי"]
rd1 --> ne1["לא נזרק חריג"]
ne1 --> nv1["פשוט לא מתקבל ערך"]
nv1 -.-> ck1["קודם בודקים את תיקיית הפלט"]
איור 11: גם כש-appsettings.json חסר, לא נזרק חריג — פשוט “לא מתקבל ערך” — אז קודם בודקים את תיקיית הפלט.
בדוגמה הזו, קוראים ישירות configuration["Sample:Message"]. אם צריך לבדוק ערך אחד או שניים, זה מספיק.
אבל כשההגדרות גדלות בעבודה מעשית, קל יותר להימנע מפיזור מחרוזות מפתח אם עוברים לצורה הבאה:
- מפרידים למחלקה לפי section
- מזריקים עם
IOptions<T> - מאמתים בזמן ההפעלה
בנוסף, בברירת המחדל של Generic Host מתחברים לא רק appsettings.json אלא גם appsettings.{Environment}.json, משתני סביבה וארגומנטים משורת הפקודה. לכן “להחליף רק בזמן פיתוח” או “לדרוס בייצור עם משתני סביבה” מתאפשרים בטבעיות רבה.
5.3. הוספת BackgroundService
בתהליך שרץ זמן רב, שימוש ב-BackgroundService הוא הבחירה הטבעית.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddScoped<PollingJob>();
builder.Services.AddHostedService<PollingWorker>();
using IHost host = builder.Build();
await host.RunAsync();
internal sealed class PollingWorker(
IServiceScopeFactory scopeFactory,
ILogger<PollingWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using PeriodicTimer timer = new(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
using IServiceScope scope = scopeFactory.CreateScope();
PollingJob job = scope.ServiceProvider.GetRequiredService<PollingJob>();
await job.RunAsync(stoppingToken);
logger.LogInformation("Polling completed.");
}
}
}
internal sealed class PollingJob(ILogger<PollingJob> logger)
{
public Task RunAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Do work here.");
return Task.CompletedTask;
}
}
בדוגמה הזו כדאי לשים לב לשתי נקודות:
- הגוף של
BackgroundServiceהואExecuteAsync - אם רוצים תלות scoped, יוצרים scope עם
IServiceScopeFactory
ל-BackgroundService עצמו אין scope כברירת מחדל. אם רוצים להשתמש בשירות scoped כמו DbContext, בטוח יותר לפתור את צד ה-job בתוך scope, כפי שמופיע למעלה.
flowchart TB
accTitle: איך משתמשים ב-scoped בתוך BackgroundService
accDescr: תרשים המראה של-BackgroundService עצמו אין scope כברירת מחדל, ולכן מזריקים IServiceScopeFactory, יוצרים scope בתוך ExecuteAsync, ופותרים שם את שירות ה-job.
bg1["BackgroundService (בלי scope כברירת מחדל)"] --> fc1["הזרקת IServiceScopeFactory"]
fc1 --> mk1["יצירת scope בתוך ExecuteAsync"]
mk1 --> rv2["פתרון ה-job בתוך ה-scope"]
איור 12: ל-BackgroundService שאין לו scope כברירת מחדל, יוצרים scope עם IServiceScopeFactory ופותרים בתוכו את ה-job.
בחירת כלי ההרצה התקופתי עצמו היא נושא נפרד, אבל אם כותבים בסגנון async, PeriodicTimer נוח למדי. זה מתחבר גם למאמר הקשור על טיימרים.
6. תבניות טיפוסיות
6.1. כלי מסוף קצר-חיים
גם ביישום שעושה עבודה פעם אחת ומסתיים — כמו batch, כלי המרה או פקודת תחזוקה — Generic Host שימושי לגמרי.
הוא מתאים למצבים כאלה:
- רוצים לקרוא קובץ תצורה
- רוצים להוציא לוגים
- רוצים להזריק
HttpClientאו repository - רוצים להחזיר קוד יציאה
בסוג הזה של יישום, אם מביאים ישירות BackgroundService ו-RunAsync(), זה קצת כבד מדי ומשתמש ביתר על ניהול אורך החיים של ה-host.
בעבודה קצרת-חיים, מספיק כמו בדוגמה המינימלית הקודמת — לפתור את JobRunner ולהריץ אותו.
flowchart TB
accTitle: הבחירה לכלי מסוף קצר-חיים
accDescr: תרשים המראה שביישום שעושה עבודה פעם אחת ומסתיים, הבאת BackgroundService ו-RunAsync היא שימוש עודף בניהול אורך החיים, ומספיק לפתור את השירות ולהריץ אותו.
on1["יישום שעושה עבודה פעם אחת ומסתיים"] -->|"מספיק"| lg1["פתרון JobRunner והרצתו"]
on1 -.->|"נוטה להיות עודף"| hv1["BackgroundService ו-RunAsync()"]
איור 13: להביא BackgroundService ליישום חד-פעמי זה עודף, ומספיק לפתור את השירות ולהריץ אותו.
6.2. worker / שירות רקע
בתהליכים כמו worker קבוע, polling, צריכת תור, ניטור או הרצה תקופתית, השילוב של Generic Host עם BackgroundService טבעי מאוד.
מה שנוח במיוחד:
- זרימת ההפעלה והעצירה מסודרת בצד ה-host
- לוגים, תצורה ו-DI זמינים מההתחלה
- קל לזרום ביטול (cancellation) עם
Ctrl+Cאו אות עצירה - קל להפריד את גוף התהליך הקבוע מ-
Program.cs
בנוסף, קל לחבר גם להקשר של Windows Service או מכולה (container). אם מפתחים את היישום כתהליך קבוע, Generic Host הוא תשתית טבעית למדי.
במעבר ל-Windows Service, פחות תקלות אם מחפשים קבצים מנקודת מוצא IHostEnvironment.ContentRootPath, ולא מהנחת הספרייה הנוכחית. הסיבה היא ש”נתיב הבסיס של היישום” נקבע בהקשר ה-host.
flowchart TB
accTitle: התשתית ל-worker קבוע
accDescr: תרשים המראה שב-worker קבוע או בהרצה תקופתית השילוב של Generic Host עם BackgroundService טבעי, וקל לחבר גם להקשר של Windows Service ומכולה.
wk1["worker קבוע / הרצה תקופתית"] --> cb1["Generic Host עם BackgroundService"]
cb1 --> ws1["שירות Windows"]
cb1 --> ct1["הרצה קבועה במכולה"]
ws1 -.-> cr1["חיפוש מנקודת מוצא ContentRootPath"]
איור 14: תהליך קבוע נוח יותר עם השילוב של host ו-BackgroundService, וקל לפתח אותו גם אל Windows Service או מכולה.
6.3. גם מתחת ל-ASP.NET Core
ביישום Web / API משתמשים ב-WebApplication.CreateBuilder(args), ולכן במבט ראשון זה יכול להיראות כעולם נפרד מ-Generic Host.
אבל התחושה בפועל מחוברת מאוד.
builder.Servicesbuilder.Configurationbuilder.Logging
הדמיון בסגנון הכתיבה נובע מכך.
ב-ASP.NET Core, גם הפעלת שרת ה-HTTP נכללת בתוך ה-lifetime של ה-host. כלומר, כשקוראים את Program.cs בצד Web, ההבנה של Generic Host עוזרת גם להבין “למה כאן נוגעים ב-DI, בתצורה ובלוגים”.
flowchart TB
accTitle: גם Web על אותו host
accDescr: תרשים המראה שגם יישום ASP.NET Core נמצא דרך WebApplication.CreateBuilder על אותה תפיסת host, ושהפעלת שרת ה-HTTP נכללת בתוך ה-lifetime של ה-host.
wa1["יישום ASP.NET Core"] --> wb2["WebApplication.CreateBuilder"]
wb2 --> gh2["אותה תפיסת host"]
gh2 -.-> ht1["גם הפעלת שרת ה-HTTP בתוך ה-lifetime"]
איור 15: גם ה-builder בצד Web נמצא על אותה תפיסת host, וגם הפעלת שרת ה-HTTP נכללת ב-lifetime.
7. מקרים שמתאימים
הנה כמה מצבים שבהם Generic Host נכנס בנוחות רבה.
- יישום מסוף שמשתמש בתצורה, לוגים ו-DI
- worker בסגנון queue consumer, poller, watchdog או scheduler
- יישום שרץ זמן רב ורוצה לנקות בעת
Ctrl+Cאו SIGTERM - יישום שעשוי לגדול בעתיד לכיוון Windows Service או הרצה קבועה במכולה
- יישום שרוצה להתאים לסגנון אותה קבוצת הרחבות כמו ASP.NET Core
המשותף לכולם הוא “לא רוצים לזלזל בנקודת הכניסה של היישום ובניהול אורך החיים שלו”.
עם זאת, קשה לצייר קו רק על סמך זה, אז נוסיף כאן גם קווים מנחים. אם מתקיימים שניים או יותר מהבאים, לרוב כדאי מההתחלה להעמיס את היישום על Generic Host — זה נוח יותר בהמשך.
flowchart TB
accTitle: קו מנחה להחלטת אימוץ
accDescr: תרשים המראה שאם מתקיימים שניים או יותר מהקווים המנחים, מעמיסים מההתחלה על Generic Host, ואם אף אחד לא מתקיים אפשר להחליט ש-Generic Host מיותר.
qn1["סופרים כמה קווים מנחים מתקיימים"] -->|"שניים או יותר"| ok1["מעמיסים מההתחלה על Generic Host"]
qn1 -->|"אף אחד"| ng1["אפשר להחליט ש-Generic Host מיותר"]
איור 16: אם מתקיימים שניים או יותר מהקווים המנחים, מעמיסים מההתחלה, ואם אף אחד לא מתקיים לא חייבים להביא אותו.
| קו מנחה | קו הגבול המעשי |
|---|---|
| מספר הגדרות | לפחות 3 הגדרות שמשתנות בין סביבות (יעד חיבור, סף, יעד פלט וכדומה) |
| לוגים | צריך לשמור לקובץ או ל-Event Log. לא מספיק לכתוב לפלט הסטנדרטי בלבד |
| צורת ההרצה | רץ באופן קבוע, או פועל לפחות פעם ביום במרווח קבוע |
| תלויות | לפחות 3 צדדים שרוצים לקבל דרך הבנאי, או שיש צד שרוצים להחליף בבדיקות |
| אורך חיים | נדרש ניקוי באמצע בעת Ctrl+C או עצירת שירות |
| עתיד | קיים סיכוי להריץ כ-Windows Service או במכולה |
8. מקרים שלא מתאימים / עודפים
מהצד השני, יש גם מצבים שבהם לא צריך להפוך את Generic Host לשחקן הראשי מההתחלה.
- כלי קטן שרק קורא ארגומנט פעם אחת, מדפיס פלט פעם אחת ומסתיים
- קוד בדיקה גס שמשתמשים בו רק כמה עשרות דקות
- פרויקט ספרייה (library)
- מקרה שבו קוראים הגדרה אחת בלבד, ולא צריך DI, לוגים או ניהול אורך חיים
כאן, כתיבה ישירה ב-Main במקום הקמת host חוסכת גם בכמות הקריאה וגם במספר הקבצים. כקו מנחה, אם אף אחד מהתנאים בטבלה שבפרק 7 לא מתקיים, אפשר להחליט ללא חשש ש-Generic Host מיותר.
חשוב להבין: חוזק של Generic Host לא הופך אותו לחובה בכל executable.
9. מוקשים נפוצים
לבסוף, נסכם נקודות שקל למעוד בהן בפעם הראשונה עם Generic Host.
- לראות ב-Generic Host רק מכל DI
- בפועל, זו תשתית שכוללת הפעלה, עצירה, תצורה, לוגים ו-hosted service.
- להתחיל מ-
Host.CreateDefaultBuilderמתוך הרגל, למרות שזה יישום חדש- אם אין סיבה להתאים לקוד קיים,
Host.CreateApplicationBuilderהוא הבחירה הטבעית יותר.
- אם אין סיבה להתאים לקוד קיים,
- להכניס שירות scoped ישירות אל
BackgroundService- ל-hosted service אין scope כברירת מחדל. בטוח יותר ליצור scope עם
IServiceScopeFactory.
- ל-hosted service אין scope כברירת מחדל. בטוח יותר ליצור scope עם
- לא להודיע ל-host על עצירה ב-worker שרץ פעם אחת בלבד
- אם עושים “run once” בתבנית Worker, בלי לקרוא ל-
IHostApplicationLifetime.StopApplication()בסיום העבודה, ה-host ימשיך לרוץ כרגיל.
- אם עושים “run once” בתבנית Worker, בלי לקרוא ל-
- לחתוך עם
Environment.Exitכשרוצים סיום תקין- אם משתמשים ב-host, כשרוצים לעצור בצורה מסודרת,
StopApplication()היא הדרך הנכונה יותר.
- אם משתמשים ב-host, כשרוצים לעצור בצורה מסודרת,
- להניח את הספרייה הנוכחית כמובנת מאליה ב-Windows Service
- יציב יותר לחפש קבצים מנקודת מוצא
IHostEnvironment.ContentRootPath.
- יציב יותר לחפש קבצים מנקודת מוצא
- לעטוף ב-
BackgroundServiceמההתחלה כלי CLI קצר-חיים- בעבודה חד-פעמית, מספיק לפתור מחלקת שירות רגילה ולהריץ אותה.
- להכניס בגסות טיימר מבוסס callback להרצה תקופתית של
BackgroundService- אם כותבים בזרימה
async,PeriodicTimerלרוב קריא יותר ופחות נוטה להתפרע.
- אם כותבים בזרימה
ב-Generic Host, חלוקה מראש בין “עבודה קצרת-חיים” ל”עבודה קבועה” לבדה מפחיתה משמעותית את הבלבול.
flowchart TB
accTitle: השאלה שמחלקים ראשונה
accDescr: תרשים המראה שקודם מחלקים בין עבודה קצרת-חיים לעבודה קבועה — בקצרת-חיים מספיק לפתור שירות ולהריץ אותו, ובקבועה משתמשים ב-BackgroundService ובניהול ה-lifetime של ה-host.
qq1["עבודה קצרת-חיים או קבועה?"] -->|"קצרת-חיים"| sj1["רק פתרון שירות והרצה"]
qq1 -->|"קבועה"| lj1["BackgroundService וניהול lifetime"]
איור 17: החלוקה הראשונית בין קצר-חיים לקבוע מפחיתה בלבול לגבי כמה מכלי ה-host להשתמש.
10. סיכום
במשפט אחד, Generic Host הוא תשתית שמאחדת את נקודת הכניסה ואת ניהול אורך החיים של יישום .NET.
נסכם את הנקודות שכדאי לזכור.
- Generic Host כולל לא רק DI, אלא גם תצורה, לוגים, טיפול בעצירה ו-hosted service
- ביישום חדש שאינו Web, טבעי להתחיל מ-
Host.CreateApplicationBuilder(args) - בעבודה קצרת-חיים, אפשר בלי
BackgroundService— רק build והרצה - בתהליך קבוע,
BackgroundServiceוניהול ה-lifetime של ה-host משמעותיים מאוד - ל-
BackgroundServiceאין scope כברירת מחדל, אז שירותים scoped דורשים יצירת scope במפורש - גם
WebApplicationBuilderשל ASP.NET Core נמצא, מבחינה רעיונית, על אותו זרם
Generic Host אינו כלי לטקס כבד. הוא כלי לרכז בפתח את התצורה, הלוגים, התלויות, ההפעלה והסיום — ברגע שהם גדלים ולו במעט — במקום לתת להם לברוח לתוך הקירות.
מהצד השני, ביישום קטן שעוד לא צריך את זה, אפשר גם לא להביא אותו. כשיודעים להבחין בין המקרים, Generic Host מפסיק להיות “משהו שמכניסים סתם”, והופך לתשתית מעשית עם ייעוד ברור.
11. מקורות
- .NET Generic Host - .NET
- Worker Services in .NET
- Use scoped services within a BackgroundService - .NET
- Configuration in .NET
- Options pattern in .NET
- .NET Generic Host in ASP.NET Core
- Create Windows Service using BackgroundService - .NET
- מאמר קשור: שלושת סוגי הטיימרים ב-.NET - מתי להשתמש ב-PeriodicTimer / Timer / DispatcherTimer
- מאמר קשור: טבלת החלטה מעשית ל-async/await ב-C# - Task.Run ו-ConfigureAwait
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
למה להשתמש ב-Generic Host וב-BackgroundService של .NET באפליקציית desktop
בכלי Windows ובאפליקציות שרצות ברקע, מסודר כאן איך להשתמש ב-Generic Host וב-BackgroundService כדי לסדר הפעלה, עיבוד תקופתי, סיום, לוג, הג...
מה זה .NET Native AOT — ההבדל מ-JIT ומ-trimming
מהו Native AOT — סקירה לפי ההבדל מ-JIT, ReadyToRun, self-contained, single-file, trimming ו-source generator, וסיכום מעשי של המקרים ש...
שלושה סוגי טיימרים ב-.NET — מתי להשתמש ב-PeriodicTimer/Timer/DispatcherTimer
מסודר כאן ההבדל בין PeriodicTimer / System.Threading.Timer / DispatcherTimer, ואיך לבחור ביניהם לעיבוד async, callback ב-ThreadPool וע...
מדריך מעשי ל-FileSystemWatcher — התמודדות עם פספוסים וכפילויות
המאמר מסדר את אופן השימוש ואת נקודות התשומת לב של FileSystemWatcher, מנקודת המבט של פספוסים, התראות כפולות, מלכודות בקביעת סיום, סריקה חו...
טבלת החלטה ל-async/await ב-C# — Task.Run ו-ConfigureAwait
המאמר מסדר את שיטות העבודה המומלצות ל-async/await ב-C#, כולל טבלת החלטה עבור המתנה ל-I/O, חישוב CPU, Task.Run, ConfigureAwait(false) ו-...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
Generic Host וארכיטקטורת יישומים
Generic Host, BackgroundService, DI, תצורה, רישום ומחזור חיי היישום.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
מדובר בהרכבה של יישום Windows שכולל תהליך רקע, טיפול בסיום, לוגים ותצורה, ולכן זהו נושא שמתאים היטב לפרויקט יישום בתחום פיתוח יישומי Windows.
ייעוץ טכני וסקירת תכנון
אם אתם רוצים לסדר DI, אורך חיים וחלוקת אחריות עוד לפני היישום, אפשר להתחיל מסידור הגישה במסגרת ייעוץ טכני וסקירת תכנון.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מה זה Generic Host?
- זו תשתית שמטפלת יחד בהפעלה ובאורך החיים של יישום .NET. בתוכה נכללים DI, תצורה (Configuration), לוגים, IHostedService / BackgroundService, וטיפול בעצירת היישום. זה לא רק עטיפה למכל DI, אלא מנגנון שמאחד את נקודת ההרכבה של היישום עם ניהול אורך החיים שלו — כך קשה לטעות בהבנה. הוא מוכיח את עצמו ביישום שבו התצורה, הלוגים, התלויות, ההפעלה והסיום גדלים ולו במעט.
- מה עדיף להשתמש בו — Host.CreateApplicationBuilder או Host.CreateDefaultBuilder?
- ביישום חדש שאינו יישום Web, טבעי להתחיל מ-Host.CreateApplicationBuilder(args). לשניהם אותה פונקציונליות ליבה ואותה התנהגות ברירת מחדל, ולא מדובר בכך שאחד הוא תכונה חדשה והשני משהו נפרד. ההבדל הוא בעיקר בסגנון הכתיבה: CreateApplicationBuilder הוא סגנון שכותבים בו ישירות אל builder.Services וכדומה, ואילו CreateDefaultBuilder הוא סגנון שמשרשרים בו קריאות כמו ConfigureServices. אם יש צורך להתאים לקוד קיים או להרכבה שמבוססת בעיקר על שיטות הרחבה ישנות, בוחרים ב-CreateDefaultBuilder.
- האם יש ערך בשימוש ב-Generic Host גם ביישום מסוף?
- אם רוצים DI, תצורה ולוגים, הוא שימושי לגמרי גם בכלי מסוף שרץ פעם אחת. אין חובה ליצור דווקא BackgroundService — אפשר לעשות Build(), לפתור את השירותים הנדרשים, ולסיים ישירות בתום העבודה, וגם כך מקבלים את היתרון של Generic Host. מהצד השני, בכלי קטן שרק קורא ארגומנט אחד, מדפיס פלט אחד ומסתיים, או בקוד בדיקה גס, זה עודף — אז לא חייבים להביא אותו בכל פעם.
- איך משתמשים בשירות scoped בתוך BackgroundService?
- ל-BackgroundService אין scope כברירת מחדל, ולכן לא בטוח להזריק שירות scoped ישירות דרך הבנאי. הצורה הבטוחה היא להזריק IServiceScopeFactory, ליצור scope במפורש בתוך ExecuteAsync, ולפתור שם את שירותי העבודה. אם רוצים להשתמש בשירות scoped כמו DbContext, חשוב במיוחד לשמור על הצורה הזו.