פרסום המקור: . עדכון המקור: . תרגום ובדיקה: .
המאמר המקורי הציג בדצמבר 2023 דרך ליצור שירות Windows עם .NET 8, בלי תלות בספריית TopShelf. המאמר הקודם על TopShelf ו־Quartz נשמר כרקע היסטורי. הדוגמה כאן משתמשת באירוח המובנה של .NET; בחרו ערכת פיתוח (SDK), גרסת יעד וחבילות שעדיין נתמכות ומתאימות זו לזו, ולא גרסה ישנה רק משום שהופיעה במקור.

החבילות ושירות הרקע
בפרויקט הוסיפו באמצעות NuGet את חבילות האירוח הבאות בגרסאות תואמות. החבילה Abstractions עשויה כבר להיכלל כתלות של חבילת האירוח; WindowsServices היא התוספת הדרושה לחיבור למנהל השירותים של Windows. ראו מדריך Microsoft לשירותי Windows.
Microsoft.Extensions.HostingMicrosoft.Extensions.Hosting.AbstractionsMicrosoft.Extensions.Hosting.WindowsServices
המחלקה MyService יורשת מ־BackgroundService, רושמת הודעה ביומן וממתינה שנייה בין איטרציות. תחביר הבנאי שבכותרת המחלקה דורש C# 12 ומעלה. אסימון הביטול stoppingToken מאפשר עצירה מסודרת; ביטול ההמתנה בזמן עצירה אינו תקלה. זו דוגמה לפעולת רקע, לא הבטחה למסירת משימות, להפעלה מחדש אחרי כשל או לפיקוח תפעולי מלא.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public class MyService(ILogger<MyService> logger) : BackgroundService
{
private readonly ILogger<MyService> _logger = logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("MyService פועל בשעה: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// ביטול צפוי בזמן עצירת השירות
}
}
}
אירוח השירות וטעינת ההגדרות
בקובץ Program.cs בנו את המארח, רשמו את שירות הרקע וחברו אותו לשירות בשם MyService. אותו שם משמש גם ברישום ב־Windows. ההגדרות נטענות לתוך builder.Configuration הקיים, במקום לרשום אובייקט הגדרות מתחרה במנגנון הזרקת התלויות:
using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.Configuration
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appSettings.json", optional: false, reloadOnChange: false);
builder.Services.AddHostedService<MyService>();
builder.Services.AddWindowsService(options =>
{
options.ServiceName = "MyService";
});
using IHost host = builder.Build();
host.Run();
השם appSettings.json נשמר במפורש מהמקור. פרסו קובץ הגדרות תקין בשם הזה לצד קובצי היישום; optional: false גורם לכשל אם הקובץ חסר. זו טעינה מפורשת מתיקיית הבסיס של היישום, ולא הסתמכות על טעינת ברירת המחדל של appsettings.json. הוספת מקור הגדרות בסוף מעניקה לו קדימות במפתחות חופפים; התאימו את סדר המקורות למדיניות הפריסה.
אפשרות נוספת: יומן עם NLog
אם בוחרים ב־NLog, הוסיפו גם את החבילה NLog.Extensions.Logging ואת מרחב השמות שלה. הגדירו מראש יעדי פלט וכללי רישום, למשל במקטע NLog של קובץ ההגדרות, והרשאות מתאימות ליעד הפלט. התיעוד של NLog מסביר את טעינת המקטע בגרסאות 5 ומעלה. הוספת ספק לבדה אינה מגדירה לאן ההודעות ייכתבו:
// הוסיפו בראש הקובץ
using NLog.Extensions.Logging;
// הוסיפו לפני הקריאה ל־builder.Build()
builder.Logging.AddNLog();
אין כאן ClearProviders(): אל תסירו ספקי יומן עובדים לפני שווידאתם שהחלופה מוגדרת. בדקו גם את רמות הסינון; ספק Windows Event Log משתמש כברירת מחדל ברמת Warning, ולכן הודעות Information מהלולאה לא בהכרח יופיעו בו.
רישום השירות ב־Windows
לפני רישום השירות, הכינו פלט פרסום ובדקו קובץ הרצה מתאים ל־Windows בסביבת הפריסה. החליפו את C:\path\to\MyService.exe בנתיב המלא של הקובץ שנפרס. רישום באמצעות New-Service הוא שינוי ניהולי הדורש הרשאה מתאימה ו־PowerShell מוגבה. בחרו חשבון שירות מאושר בעל ההרשאות המזעריות הנדרשות; אל תשאירו את זהותו משתמעת ואל תניחו שנדרש LocalSystem.
הדוגמה הבאה מתאימה לחשבון שירות רגיל עם פרטי כניסה, שהרשאת הכניסה שלו כשירות והגישה הדרושה לקובצי היישום כבר הוסדרו. הפרטים מוזנים באמצעות Get-Credential, לא כסיסמה גלויה בקוד. חשבון שירות מנוהל או וירטואלי מחייב נוהל פריסה מתאים, ולא העתקה של קטע הקוד הזה:
$credential = Get-Credential -Message 'פרטי החשבון שאושר להפעלת השירות'
if ($null -eq $credential) {
throw 'לא נבחר חשבון שירות.'
}
$params = @{
Name = 'MyService'
BinaryPathName = '"C:\path\to\MyService.exe"'
StartupType = 'Automatic'
Credential = $credential
}
New-Service @params -WhatIf
המרכאות הכפולות הן חלק מערך BinaryPathName ומגינות על פירוש נתיב שמכיל רווחים. -WhatIf מציג את הפעולה המתוכננת בלי ליצור את השירות; בקשת פרטי הכניסה שלפניו עדיין מוצגת. יצירה בפועל בלי הדגל הזה שייכת לפריסה מורשית בלבד. Automatic היא בחירה מכוונת להפעלה בעת אתחול המערכת, לא תנאי להידור של היישום.
תיקיית עבודה אינה שורש התוכן
תיקיית העבודה של שירות עשויה להיות System32, אבל היא אינה בהכרח שורש התוכן (ContentRootPath) של המארח. החל מ־.NET 7, ברירת המחדל של האירוח מטפלת במקרה שבו תיקיית העבודה היא תיקיית המערכת ומשתמשת בתיקיית הבסיס של היישום לשורש התוכן. ראו תיאור השינוי של Microsoft. בדוגמה למעלה גם נקבע נתיב בסיס מפורש לקובץ ההגדרות.
הקריאה הבאה נשמרת מהמקור כאפשרות לשינוי תיקיית העבודה של התהליך כולו, ולא כצעד חובה. היא דורשת using System.IO; בראש הקובץ:
Directory.SetCurrentDirectory(System.AppDomain.CurrentDomain.BaseDirectory);
שינוי כזה משפיע על כל שימוש בנתיבים יחסיים בתהליך. העדיפו לבנות נתיבים במפורש מתוך AppContext.BaseDirectory או מתוך שורש התוכן המתאים, במקום להניח שקובץ הגדרות יהיה בלתי נגיש ללא שינוי תיקיית העבודה.
דרך חלופית לתזמון המשימה המחזורית
לתזמון לפי לוח זמנים אפשר לבחור בחבילת NCronJob ובמרחב השמות שלה. זהו מסלול חלופי ללולאת שירות הרקע: החליפו את AddHostedService<MyService>() ברישום הבא, לפני בניית המארח. השאירו את יתר הגדרות האירוח. התיעוד של הפרויקט מציין ש־AddJob רושם את המשימה עם תחום תלויות חדש לכל הפעלה (scoped), ולכן אין צורך ברישום AddTransient נוסף:
// הוסיפו בראש הקובץ
using NCronJob;
// לפני builder.Build(), במקום AddHostedService<MyService>()
builder.Services.AddNCronJob(options =>
options.AddJob<MyWorkerService>(j =>
{
// כלי עזר לקריאת הביטוי: https://crontab.guru/
j.WithCronExpression("0/30 * * * *");
})
);
הביטוי בן חמשת השדות 0/30 * * * * מציין את הדקות 0 ו־30 בכל שעה — לא כל 30 שניות. לפי תיעוד הפרויקט, אזור הזמן המוגדר כברירת מחדל הוא זמן אוניברסלי מתואם (UTC); אם נדרש אזור זמן אחר, הגדירו אותו במפורש. crontab.guru הוא כלי עזר לקריאת הביטוי, לא תיעוד המימוש של הספרייה.
צרו את מחלקת המשימה. גוף הפעולה נשאר מציין מקום, כפי שהיה במקור; מאחר שאין בו פעולה אסינכרונית בפועל, מוחזרת Task.CompletedTask בלי async מיותר:
using System.Threading;
using System.Threading.Tasks;
using NCronJob;
public class MyWorkerService : IJob
{
public Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
// מקום לפעולה האמיתית; הדוגמה אינה מבצעת עבודה
return Task.CompletedTask;
}
}
כאשר מוסיפים את הפעולה האמיתית, העבירו אליה את אסימון הביטול לפי הצורך. המתזמן הזה אינו שומר משימות בין הפעלות מחדש או היסטוריית ביצוע; אל תסיקו מהדוגמה הבטחה לשחזור עבודה שאבדה.
תרגום ועיבוד מתוקן: נשמרו כל נושאי המקור ודוגמאות השירות, ההגדרות, היומן והתזמון. נוספו החבילות החסרות ותנאי הפריסה, אוחד שם השירות, הובהרה בחירת חשבון השירות ותוקנו ההסברים על תיקיית העבודה והרישום של NCronJob. הקוד הוא עיבוד מתוקן של הדוגמאות ההיסטוריות.
תרגום לעברית עבור The Useful Byte. לקריאת המקור באנגלית.