使用
.NET Framework 開發人員可能熟悉 Windows 服務應用程式。 在 .NET Core 和 .NET 5+ 之前,依賴 .NET Framework 的開發人員可以建立 Windows 服務來執行背景工作或執行長時間執行的進程。 這項功能仍然可用,您可以建立以 Windows 服務身分執行的背景工作服務。
在本教學課程中,您將瞭解如何:
- 將 .NET 工作者應用程式發佈為單一檔案可執行檔。
- 建立 Windows 服務。
- 建立
BackgroundService
應用程式作為 Windows 服務。 - 啟動和停止 Windows 服務。
- 檢視事件記錄檔。
- 刪除 Windows 服務。
提示
所有 「.NET 工作者」範例原始程式碼都可在 範例瀏覽器 下載。 如需詳細資訊,請參閱 瀏覽程式碼範例:.NET 中的工作者。
重要
安裝 .NET SDK 時,也會安裝 Microsoft.NET.Sdk.Worker
和工作程序範本。 換句話說,安裝 .NET SDK 之後,您可以使用 dotnet new worker 命令來建立新的 worker。 如果您使用 Visual Studio,範本會隱藏,直到安裝選擇性 ASP.NET 和 Web 開發工作負載為止。
先決條件
- .NET 8.0 SDK 或更新版本
- 一個 Windows 作業系統
- .NET 集成開發環境 (IDE)
- 您可以隨意使用 Visual Studio
建立新專案
若要使用 Visual Studio 建立新的背景工作服務專案,請選取 [檔案]>[新增>專案...]。從 [[建立新專案] 對話框搜尋 [背景工作服務],然後選取 [背景工作服務] 範本。 如果您想要使用 .NET CLI,請在工作目錄中開啟您最愛的終端機。 執行 dotnet new
命令,並將 <Project.Name>
取代為您所需的項目名稱。
dotnet new worker --name <Project.Name>
如需 .NET CLI 新工作服務專案命令的詳細資訊,請參閱 dotnet new worker。
提示
如果您使用 Visual Studio Code,您可以從整合式終端機執行 .NET CLI 命令。 如需詳細資訊,請參閱 Visual Studio Code:整合式終端機。
安裝 NuGet 套件
若要讓 .NET IHostedService 實作與原生 Windows 服務進行互操作,您需要安裝 Microsoft.Extensions.Hosting.WindowsServices
NuGet 套件。
若要從 Visual Studio 安裝此專案,請使用 [管理 NuGet 套件 ] 對話方塊。 搜尋 「Microsoft.Extensions.Hosting.WindowsServices」,並加以安裝。 如果您想要使用 .NET CLI,請執行下列命令。 (如果您使用 .NET 9 或更早版本的 SDK 版本,請改用 dotnet add package
表單。
dotnet package add Microsoft.Extensions.Hosting.WindowsServices
如需詳細資訊,請參閱 dotnet package add。
成功新增套件之後,您的專案檔現在應該包含下列套件參考:
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.6" />
</ItemGroup>
更新項目檔
此工作專案會使用 C# 可為 Null 參考型別。 若要針對整個專案啟用這些專案,請據以更新項目檔:
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
<RootNamespace>App.WindowsService</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.6" />
</ItemGroup>
</Project>
上述項目檔變更會新增 <Nullable>enable<Nullable>
節點。 如需詳細資訊,請參閱 設定可空上下文。
建立服務
將新的類別新增至名為 JokeService.cs的專案,並以下列 C# 程式代碼取代其內容:
namespace App.WindowsService;
public sealed class JokeService
{
public string GetJoke()
{
Joke joke = _jokes.ElementAt(
Random.Shared.Next(_jokes.Count));
return $"{joke.Setup}{Environment.NewLine}{joke.Punchline}";
}
// Programming jokes borrowed from:
// https://github.com/eklavyadev/karljoke/blob/main/source/jokes.json
private readonly HashSet<Joke> _jokes = new()
{
new Joke("What's the best thing about a Boolean?", "Even if you're wrong, you're only off by a bit."),
new Joke("What's the object-oriented way to become wealthy?", "Inheritance"),
new Joke("Why did the programmer quit their job?", "Because they didn't get arrays."),
new Joke("Why do programmers always mix up Halloween and Christmas?", "Because Oct 31 == Dec 25"),
new Joke("How many programmers does it take to change a lightbulb?", "None that's a hardware problem"),
new Joke("If you put a million monkeys at a million keyboards, one of them will eventually write a Java program", "the rest of them will write Perl"),
new Joke("['hip', 'hip']", "(hip hip array)"),
new Joke("To understand what recursion is...", "You must first understand what recursion is"),
new Joke("There are 10 types of people in this world...", "Those who understand binary and those who don't"),
new Joke("Which song would an exception sing?", "Can't catch me - Avicii"),
new Joke("Why do Java programmers wear glasses?", "Because they don't C#"),
new Joke("How do you check if a webpage is HTML5?", "Try it out on Internet Explorer"),
new Joke("A user interface is like a joke.", "If you have to explain it then it is not that good."),
new Joke("I was gonna tell you a joke about UDP...", "...but you might not get it."),
new Joke("The punchline often arrives before the set-up.", "Do you know the problem with UDP jokes?"),
new Joke("Why do C# and Java developers keep breaking their keyboards?", "Because they use a strongly typed language."),
new Joke("Knock-knock.", "A race condition. Who is there?"),
new Joke("What's the best part about TCP jokes?", "I get to keep telling them until you get them."),
new Joke("A programmer puts two glasses on their bedside table before going to sleep.", "A full one, in case they gets thirsty, and an empty one, in case they don’t."),
new Joke("There are 10 kinds of people in this world.", "Those who understand binary, those who don't, and those who weren't expecting a base 3 joke."),
new Joke("What did the router say to the doctor?", "It hurts when IP."),
new Joke("An IPv6 packet is walking out of the house.", "He goes nowhere."),
new Joke("3 SQL statements walk into a NoSQL bar. Soon, they walk out", "They couldn't find a table.")
};
}
readonly record struct Joke(string Setup, string Punchline);
上述笑話服務的原始程式碼公開了一項功能,即 GetJoke
方法。 這是一個 string
的返回方法,代表隨機的編程笑話。 類別範圍的 _jokes
欄位用來儲存笑話清單。 從清單中選取隨機笑話並傳回。
重寫 Worker
類別
使用下列 C# 程式代碼取代範本中的現有 Worker
,並將檔案重新命名為 WindowsBackgroundService.cs:
namespace App.WindowsService;
public sealed class WindowsBackgroundService(
JokeService jokeService,
ILogger<WindowsBackgroundService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
while (!stoppingToken.IsCancellationRequested)
{
string joke = jokeService.GetJoke();
logger.LogWarning("{Joke}", joke);
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
catch (OperationCanceledException)
{
// When the stopping token is canceled, for example, a call made from services.msc,
// we shouldn't exit with a non-zero exit code. In other words, this is expected...
}
catch (Exception ex)
{
logger.LogError(ex, "{Message}", ex.Message);
// Terminates this process and returns an exit code to the operating system.
// This is required to avoid the 'BackgroundServiceExceptionBehavior', which
// performs one of two scenarios:
// 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
// 2. When set to "StopHost": will cleanly stop the host, and log errors.
//
// In order for the Windows Service Management system to leverage configured
// recovery options, we need to terminate the process with a non-zero exit code.
Environment.Exit(1);
}
}
}
在上述程式代碼中,JokeService
會與 ILogger
一起插入。 這兩者都可以以欄位的形式提供給類別。 在 ExecuteAsync
方法中,笑話服務會要求一個笑話,並將它寫入記錄器。 在這裡情況下,記錄器是由 Windows 事件記錄檔實作 - Microsoft.Extensions.Logging.EventLog.EventLogLoggerProvider。 記錄檔會寫入,且可在 事件查看器中檢視。
注意
根據預設,事件記錄檔 嚴重性為 Warning。 這可以設定,但為了示範的目的,會使用 WindowsBackgroundService
擴充方法來記錄 LogWarning。 若要特別以 EventLog
層級為目標,請在 appsettings.{Environment}.json 中新增條目,或提供 EventLogSettings.Filter 值。
{
"Logging": {
"LogLevel": {
"Default": "Warning"
},
"EventLog": {
"SourceName": "The Joke Service",
"LogName": "Application",
"LogLevel": {
"Microsoft": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
}
如需設定記錄層級的詳細資訊,請參閱 .NET 中的 記錄提供者:設定 Windows EventLog。
重寫 Program
類別
使用下列 C# 程式代碼取代樣本 Program.cs 檔案內容:
using App.WindowsService;
using Microsoft.Extensions.Logging.Configuration;
using Microsoft.Extensions.Logging.EventLog;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(options =>
{
options.ServiceName = ".NET Joke Service";
});
LoggerProviderOptions.RegisterProviderOptions<
EventLogSettings, EventLogLoggerProvider>(builder.Services);
builder.Services.AddSingleton<JokeService>();
builder.Services.AddHostedService<WindowsBackgroundService>();
IHost host = builder.Build();
host.Run();
AddWindowsService
擴充方法會將應用程式設定為做為 Windows 服務。 服務名稱設定為 ".NET Joke Service"
。 托管的服務已註冊為相依注入。
如需註冊服務的詳細資訊,請參閱 .NET 中的相依性插入 。
發佈應用程式
若要將 .NET 背景工作服務應用程式建立為 Windows 服務,建議您將應用程式發佈為單一檔案可執行檔。 由於沒有相依檔案散佈在系統中,因此擁有一個自包含的可執行檔較不容易出錯。 但是,您可以選擇不同的發佈方式,這是完全可接受的,只要您建立 *.exe 檔案,Windows 服務控制管理員可以設為目標。
重要
另一種發佈方法是建置 *.dll(而不是 *.exe),當您使用 Windows 服務控制管理員安裝已發佈的應用程式時,將操作委派給 .NET CLI,並傳遞 DLL。 如需詳細資訊,請參閱 .NET CLI:dotnet 命令。
sc.exe create ".NET Joke Service" binpath= "C:\Path\To\dotnet.exe C:\Path\To\App.WindowsService.dll"
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
<RootNamespace>App.WindowsService</RootNamespace>
<OutputType>exe</OutputType>
<PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.6" />
</ItemGroup>
</Project>
項目檔的前幾行醒目提示會定義下列行為:
-
<OutputType>exe</OutputType>
:建立主控台應用程式。 -
<PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
:啟用單一檔案發佈。 -
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
:指定的win-x64
。 -
<PlatformTarget>x64</PlatformTarget>
:指定64位的目標平臺CPU。
若要從 Visual Studio 發佈應用程式,您可以建立保存的發行配置檔。 發行配置檔是以 XML 為基礎,且副檔名為 .pubxml。 Visual Studio 會使用此設定檔以隱含方式發佈應用程式,而如果您使用 .NET CLI,則必須明確指定要使用的發行配置檔。
以滑鼠右鍵點擊 [方案總管]中的項目,然後選取 [發行]。 然後,選取 新增發佈配置檔 以建立配置檔。 從 [發佈] 對話框中,選取 [資料夾] 作為 [目標]。
保留預設 位置,然後選擇 完成。 建立設定檔之後,請選擇 [顯示所有設定],然後確認您的 [設定檔設定]。
請確定已指定下列設定:
- 部署模式:獨立式
- 產生單一檔案: 已核取
- 啟用 ReadyToRun 編譯: 已核取
- 修剪未使用的組件(預覽):未勾選
最後,選取 發行。 應用程式會編譯,產生的 .exe 檔案會發行至 /publish 輸出目錄。
或者,您可以使用 .NET CLI 來發佈應用程式:
dotnet publish --output "C:\custom\publish\directory"
如需詳細資訊,請參閱 dotnet publish
。
重要
使用 .NET 6 時,如果您嘗試使用 <PublishSingleFile>true</PublishSingleFile>
設定對應用程式進行偵錯,您將無法對應用程式進行偵錯。 如需更多資訊,請參閱調試『PublishSingleFile』.NET 6 應用程式時無法附加至 CoreCLR 的問題 。
建立 Windows 服務
如果您不熟悉使用 PowerShell,而且寧願為服務建立安裝程式,請參閱 建立 Windows 服務安裝程式。 否則,若要建立 Windows 服務,請使用原生 Windows 服務控制管理員的 (sc.exe) create 命令。 以系統管理員身分執行 PowerShell。
sc.exe create ".NET Joke Service" binpath= "C:\Path\To\App.WindowsService.exe"
提示
如果您需要變更 主機組態的內容根目錄,您可以在指定 binpath
時,將它當做命令行自變數傳遞:
sc.exe create "Svc Name" binpath= "C:\Path\To\App.exe --contentRoot C:\Other\Path"
您會看到輸出訊息:
[SC] CreateService SUCCESS
如需詳細資訊,請參閱 sc.exe 建立。
設定 Windows 服務
建立服務之後,您可以選擇性地進行設定。 如果您對於服務的預設設定感到滿意,請跳至 [驗證服務功能] 區段。
Windows 服務提供復原組態選項。 您可以使用 sc.exe qfailure "<Service Name>"
查詢目前的組態(其中 <Service Name>
是您服務的名稱)命令來讀取目前的復原組態值:
sc qfailure ".NET Joke Service"
[SC] QueryServiceConfig2 SUCCESS
SERVICE_NAME: .NET Joke Service
RESET_PERIOD (in seconds) : 0
REBOOT_MESSAGE :
COMMAND_LINE :
命令會輸出復原組態,這是預設值,因為它們尚未設定。
若要設定復原,請使用 sc.exe failure "<Service Name>"
,其中 <Service Name>
是您服務的名稱:
sc.exe failure ".NET Joke Service" reset= 0 actions= restart/60000/restart/60000/run/1000
[SC] ChangeServiceConfig2 SUCCESS
提示
若要設定復原選項,您的命令列視窗必須以系統管理員身分執行。
成功設定之後,您可以使用 sc.exe qfailure "<Service Name>"
命令再次查詢值:
sc qfailure ".NET Joke Service"
[SC] QueryServiceConfig2 SUCCESS
SERVICE_NAME: .NET Joke Service
RESET_PERIOD (in seconds) : 0
REBOOT_MESSAGE :
COMMAND_LINE :
FAILURE_ACTIONS : RESTART -- Delay = 60000 milliseconds.
RESTART -- Delay = 60000 milliseconds.
RUN PROCESS -- Delay = 1000 milliseconds.
您會看到已設定的重新啟動值。
服務復原選項和 .NET BackgroundService
實例
使用 .NET 6,新的托管異常處理行為 已新增至 .NET。
BackgroundServiceExceptionBehavior 列舉已新增至 Microsoft.Extensions.Hosting
命名空間,用於指定當擲出例外狀況時服務的行為。 下表列出可用的選項:
選擇 | 描述 |
---|---|
Ignore | 忽略在 BackgroundService 中拋出的例外狀況。 |
StopHost | 當出現未處理的例外時,IHost 將會停止。 |
.NET 6 之前的預設行為是 Ignore
,這會導致 殭屍程序(未執行任何動作的程序)。 在使用 .NET 6 時,預設行為是 StopHost
,這會導致在拋出例外狀況時主機停止運作。 但它會完全停止,這表示 Windows 服務管理系統不會重新啟動服務。 若要正確允許重新啟動服務,您可以使用非零結束代碼呼叫 Environment.Exit。 請考慮下列醒目的 catch
區塊:
namespace App.WindowsService;
public sealed class WindowsBackgroundService(
JokeService jokeService,
ILogger<WindowsBackgroundService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
while (!stoppingToken.IsCancellationRequested)
{
string joke = jokeService.GetJoke();
logger.LogWarning("{Joke}", joke);
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
catch (OperationCanceledException)
{
// When the stopping token is canceled, for example, a call made from services.msc,
// we shouldn't exit with a non-zero exit code. In other words, this is expected...
}
catch (Exception ex)
{
logger.LogError(ex, "{Message}", ex.Message);
// Terminates this process and returns an exit code to the operating system.
// This is required to avoid the 'BackgroundServiceExceptionBehavior', which
// performs one of two scenarios:
// 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
// 2. When set to "StopHost": will cleanly stop the host, and log errors.
//
// In order for the Windows Service Management system to leverage configured
// recovery options, we need to terminate the process with a non-zero exit code.
Environment.Exit(1);
}
}
}
驗證服務功能
若要查看建立為 Windows 服務的應用程式,請開啟 Services。 選取 Windows 鍵(或 Ctrl + Esc),然後從 [服務] 搜尋。 從 Services 應用程式中,您應該能夠依其名稱尋找您的服務。
重要
根據預設,一般(非系統管理員)用戶無法管理 Windows 服務。 若要確認此應用程式如預期般運作,您必須使用系統管理員帳戶。
若要確認服務如預期般運作,您需要:
- 啟動服務
- 檢視記錄
- 停止服務
重要
若要偵錯應用程式,請確定您 未 嘗試偵錯在 Windows 服務程序中正在執行的可執行檔。
啟動 Windows 服務
若要啟動 Windows 服務,請使用 sc.exe start
命令:
sc.exe start ".NET Joke Service"
您會看到類似下列的輸出:
SERVICE_NAME: .NET Joke Service
TYPE : 10 WIN32_OWN_PROCESS
STATE : 2 START_PENDING
(NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x7d0
PID : 37636
FLAGS
服務 狀態 會從 START_PENDING
轉換至 執行中。
檢視記錄
若要檢視記錄檔,請開啟 事件檢視器。 選取 Windows 鍵 (或 Ctrl + Esc),然後搜尋 "Event Viewer"
。 選取 事件查看器 [本機]>Windows 記錄>應用程式 節點。 您應該會看到 警告 層級條目,且 Source 與應用程式的命名空間相匹配。 按兩下項目,或以滑鼠右鍵按下,然後選取 [事件屬性] 以查看詳細資料。
在 事件記錄檔中看到記錄之後,您應該停止服務。 它的設計目的是每分鐘記錄一次隨機的笑話。 這是刻意的行為,但對於生產服務而言,缺乏 實用性。
停止 Windows 服務
若要停止 Windows 服務,請使用 sc.exe stop
命令:
sc.exe stop ".NET Joke Service"
您會看到類似下列的輸出:
SERVICE_NAME: .NET Joke Service
TYPE : 10 WIN32_OWN_PROCESS
STATE : 3 STOP_PENDING
(STOPPABLE, NOT_PAUSABLE, ACCEPTS_SHUTDOWN)
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x0
服務 狀態 會從 STOP_PENDING
轉換為已停止 。
刪除 Windows 服務
若要刪除 Windows 服務,請使用原生 Windows Service Control Manager 的 (sc.exe) delete 命令。 以系統管理員身分執行 PowerShell。
重要
如果服務未處於 已停止 狀態,將不會立即刪除。 在發出 delete 命令之前,請確定服務已停止。
sc.exe delete ".NET Joke Service"
您會看到輸出訊息:
[SC] DeleteService SUCCESS
如需詳細資訊,請參閱 sc.exe 刪除。
另請參閱
- 建立 Windows 服務安裝程式
- .NET 中的 工作者服務
- 建立佇列服務
-
在
BackgroundService
中使用範圍服務 -
實作
IHostedService
介面