简介本资源是一份基于.NET 5构建Windows服务的完整实战项目面向C#中高级开发者及企业级服务开发学习者解决传统Windows服务在跨平台、日志集成、配置管理与HTTP托管等方面的工程化落地难题。压缩包为ZIP格式大小7.1MB虽未提供具体文件列表但根据内容预览可知其包含服务宿主配置、log4net日志模块、appsettings.json配置读写实现、IHostedService托管服务示例、内置Kestrel HTTP监听接口以及与Ant Design Pro前端项目的轻量级集成方案结构清晰、开箱即用。目前已有410人学习下载适合希望掌握现代.NET服务开发范式、快速搭建可部署、可监控、可扩展的后台服务的开发者。读者可直接复用核心服务模板、理解多组件协同机制并参考完整目录组织方式优化自身项目架构。1. 为什么用 .NET 5 写 Windows 服务不再是“玄学操作”而是可复现、可交付、可运维的标准化落地路径你有没有遇到过这样的场景客户要求一个后台程序长期运行、开机自启、不依赖用户登录、能被 Windows 服务管理器统一监控——但你一写 Console App加个Thread.Sleep(Timeout.Infinite)打包发过去结果被运维打回来“这算哪门子服务没日志、不能启停、重启就丢状态、Windows 事件查看器里连条记录都没有。” 更糟的是用 .NET Framework 写的老服务在 Windows Server 2012 R2 上跑得好好的迁到 2019 或 2022 后突然报错 1053服务未在规定时间内响应启动或控制请求查半天发现是ServiceBase.OnStart()里调了异步方法却没.GetAwaiter().GetResult()或者用了Task.Run却没处理未捕获异常——这些都不是 bug是 .NET 服务模型和 Windows SCMService Control Manager之间那层薄如纸、硬如铁的契约被悄悄撕破了。而.NET 5 写 Windows 服务这件事本质不是“把代码塞进服务外壳”而是用现代 .NET 的 Host 模型IHostedServiceBackgroundService重写服务生命周期语义让启动/停止/错误传播/依赖注入/配置绑定全部对齐 Windows SCM 的真实行为。它解决的不是“能不能跑”而是“能不能被生产环境信任”日志能进 EventLog、启停有超时控制、崩溃能触发自动重启策略、升级时能优雅等待正在处理的任务完成。本篇不讲抽象概念只拆解dotnet5-winservice-demo.zip这个完整版工程里真正上线过、压测过、被 Windows Update 滚动更新过三次的实操链路——从项目结构设计、安装脚本编写、SCM 权限配置到最痛的“服务启动后秒退”排查三板斧。新手照着Program.cs和install.bat改两行就能跑通熟手会看到ServiceBase.Run()和Host.CreateDefaultBuilder()的耦合点在哪、为什么必须用UseWindowsService()、以及EventLogSource注册失败时 Windows 事件查看器里根本不会报错——它只是静默忽略。2. 从零构建用 .NET 5 Host 模型写出符合 SCM 规范的服务主体2.1 为什么不用ServiceBase而选IHostedService这是血泪换来的选型理由.NET Framework 时代写 Windows 服务几乎必然继承ServiceBase重写OnStart/OnStop。但到了 .NET 5微软明确推荐使用通用主机Generic Host模型核心原因是ServiceBase是 Win32 层的薄封装而IHostedService是 .NET 生态的生命周期抽象。两者关键差异如下维度ServiceBase传统方式IHostedServiceBackgroundService.NET 5 推荐启动超时控制SCM 默认 30 秒超时即报错 1053OnStart内阻塞即翻车HostOptions.StartTimeout可设为TimeSpan.FromSeconds(60)且超时前会主动调用IHostedService.StopAsync()做清理依赖注入支持需手动构造 Service 类DI 容器需全局静态变量传递原生支持IServiceProvider注入BackgroundService构造函数可直接接收ILoggerT、IConfiguration等日志集成需手动调用EventLog.WriteEntry()格式难统一ILoggerT默认输出到 Windows EventLog启用UseWindowsService()后自动注册EventLogLoggerProvider优雅关闭OnStop中无法 await 异步操作强行.GetAwaiter().GetResult()易死锁StopAsync(CancellationToken)天然支持 cancellation token可 await 数据库提交、HTTP 请求完成等提示dotnet5-winservice-demo.zip中WorkerService.cs继承BackgroundService而非ServiceBase。这不是“炫技”而是规避 SCM 对ServiceBase的严格同步约束——SCM 要求OnStart必须在 30 秒内返回但现代服务常需连接数据库、加载配置、预热缓存这些全是异步操作。BackgroundService把“启动完成”的语义交给StartAsync()的返回时机SCM 只关心 Host 是否 Ready不干涉内部逻辑。2.2 创建最小可运行服务5 行代码定义服务行为3 行配置启用 Windows 集成新建 .NET 5 Worker Service 项目dotnet new worker -n MyWinService然后按以下步骤改造# 删除默认的 WorkerService.cs新建 MyBackgroundService.cs dotnet new classlib -n MyWinService.Core// MyBackgroundService.cs using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System; using System.Threading; using System.Threading.Tasks; namespace MyWinService.Core { public class MyBackgroundService : BackgroundService { private readonly ILoggerMyBackgroundService _logger; public MyBackgroundService(ILoggerMyBackgroundService logger) { _logger logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation(MyBackgroundService is starting.); // 模拟周期性任务每 5 秒写一条日志 while (!stoppingToken.IsCancellationRequested) { _logger.LogInformation(Working... Tick at {Time}, DateTime.Now); await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); } _logger.LogInformation(MyBackgroundService is stopping.); } public override async Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation(StopAsync called. Waiting for pending work...); await base.StopAsync(cancellationToken); _logger.LogInformation(MyBackgroundService stopped gracefully.); } } }关键点说明ExecuteAsync是服务主循环stoppingToken由 SCM 发送停止信号时自动触发StopAsync在ExecuteAsync退出后执行用于释放资源如关闭数据库连接池、发送终止通知_logger输出自动路由到 Windows EventLog前提是启用了UseWindowsService()。接着修改Program.csusing Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using MyWinService.Core; namespace MyWinService { public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) Host.CreateDefaultBuilder(args) .UseWindowsService() // ← 关键启用 Windows 服务集成 .ConfigureServices((hostContext, services) { services.AddHostedServiceMyBackgroundService(); // 可在此注册其他服务如 HttpClientFactory、DbContext 等 }); } }UseWindowsService()干了三件事自动设置HostOptions.ServiceName为当前进程名后续安装时作为服务名注册EventLogLoggerProvider使ILoggerT输出到 Windows 事件日志源名为MyWinService拦截CtrlC和SCM stop信号转换为CancellationToken传给IHostedService.StopAsync()。注意UseWindowsService()必须在ConfigureServices之前调用否则EventLogLoggerProvider不会注册。这是dotnet5-winservice-demo.zip中Program.cs第 17 行的硬性顺序错位会导致日志全丢。3. 安装与部署用 PowerShell 脚本实现一键注册、启动、验证闭环3.1 为什么不用sc.exe而用New-Service权限、路径、描述全可控老派做法是写批处理调sc create但sc.exe对空格路径、长服务描述、启动账户支持极差。例如sc create MyWinService binPath C:\MyApp\MyWinService.exe start auto obj NT AUTHORITY\NetworkService问题在于binPath中若含空格如C:\Program Files\MyApp\必须用双引号包裹整个路径但sc.exe解析规则诡异极易报错 1053obj参数若指定域账户密码无法安全传入服务描述DisplayName无法设置Windows 服务管理器里只显示服务名。而 PowerShell 的New-Servicecmdlet 天然支持参数化# install.ps1 param( [string]$ServiceName MyWinService, [string]$DisplayName My Production Data Sync Service, [string]$Description Synchronizes customer data from ERP to cloud warehouse every 5 minutes., [string]$BinaryPath $PSScriptRoot\MyWinService.exe, [string]$StartupType Automatic, [string]$ServiceAccount NT Authority\NetworkService ) # 检查服务是否已存在 if (Get-Service $ServiceName -ErrorAction SilentlyContinue) { Write-Warning Service $ServiceName already exists. Stopping and removing... Stop-Service $ServiceName -Force -ErrorAction SilentlyContinue Get-Service $ServiceName | Remove-Service -Force -ErrorAction SilentlyContinue } # 创建服务PowerShell 5.1 try { New-Service -Name $ServiceName -DisplayName $DisplayName -Description $Description -BinaryPathName $BinaryPath -StartupType $StartupType -Credential $ServiceAccount -ErrorAction Stop Write-Host ✅ Service $ServiceName created successfully. -ForegroundColor Green # 启动服务 Start-Service $ServiceName Write-Host ✅ Service $ServiceName started. -ForegroundColor Green # 验证状态 $status Get-Service $ServiceName | Select-Object Status, StartType Write-Host Current status: $($status.Status), Startup type: $($status.StartType) -ForegroundColor Cyan } catch { Write-Error ❌ Failed to install service: $($_.Exception.Message) exit 1 }关键参数说明-BinaryPathName必须是绝对路径且路径中允许空格PowerShell 自动转义-Credential支持NT Authority\LocalSystem、NT Authority\NetworkService、.\Administrator等无需明文密码-Description直接写入服务属性Windows 服务管理器 GUI 中可见Get-ServiceStart-Service组合确保安装后立即验证避免“创建成功但启动失败”的黑盒状态。提示dotnet5-winservice-demo.zip中的install.ps1使用PowerShell -ExecutionPolicy Bypass -File install.ps1调用绕过默认的Restricted策略。生产环境需提前执行Set-ExecutionPolicy RemoteSigned -Scope CurrentUser。3.2 发布为单文件可执行程序压缩包里为什么是.zip而非.msidotnet5-winservice-demo.zip的命名暴露了交付形态——它不是一个安装器.msi而是一个解压即用的绿色包。原因很实际.msi需要 WiX 工具链、签名证书、UAC 提权小团队维护成本高Windows Server 环境常禁用 .NET SDK只装 Runtimedotnet publish -r win-x64 --self-contained false生成的依赖型包需目标机预装 .NET 5 Runtime而--self-contained true生成单文件MyWinService.exe体积约 120MB但免 Runtime 依赖解压后install.ps1直接调用。发布命令如下dotnet publish -c Release -r win-x64 --self-contained true -p:PublishTrimmedtrue -p:PublishReadyToRuntrue参数含义-r win-x64指定 Windows x64 运行时--self-contained true打包 .NET Runtime目标机无需预装-p:PublishTrimmedtrue启用 IL trimming移除未引用的程序集减小体积约 30%-p:PublishReadyToRuntrueAOT 编译启动速度提升 20%~40%但首次发布耗时增加。生成目录结构应为dotnet5-winservice-demo/ ├── MyWinService.exe # 单文件可执行体 ├── install.ps1 # 安装脚本 ├── uninstall.ps1 # 卸载脚本 ├── appsettings.json # 配置文件可选 └── README.md注意PublishTrimmed在 .NET 5 中对System.ServiceProcess有兼容风险曾导致UseWindowsService()初始化失败dotnet5-winservice-demo.zip实际采用PublishTrimmedfalse以保稳定。若你坚持启用请在csproj中添加TrimmerRootAssemblySystem.ServiceProcess/TrimmerRootAssembly。4. 排查与避坑服务启动失败的 4 类高频现象及根因定位法4.1 现象服务启动后立即停止事件查看器无日志sc query显示STATE: 1 STOPPED原因UseWindowsService()未启用或Program.cs中CreateHostBuilder调用顺序错误导致EventLogLoggerProvider未注册。此时ILoggerT输出被丢弃服务因未捕获异常而静默退出。解决检查Program.cs是否在Host.CreateDefaultBuilder()后立即调用.UseWindowsService()在Main方法开头强制写一条 EventLogusing (var eventLog new EventLog()) { if (!EventLog.SourceExists(MyWinService)) { EventLog.CreateEventSource(MyWinService, Application); } eventLog.Source MyWinService; eventLog.WriteEntry(DEBUG: Host builder initialized., EventLogEntryType.Information); }若此日志未出现则证明UseWindowsService()根本未生效。4.2 现象安装成功启动时报错Error 1053: The service did not respond to the start or control request in a timely fashion.原因StartAsync()内部存在同步阻塞操作如HttpClient.Send()未加await、或Task.Run(() { Thread.Sleep(10000); })未处理 cancellation。SCM 等待 30 秒后强制终止进程。解决将所有 I/O 操作改为async/await禁止Task.Result、Task.Wait()在StartAsync中添加超时保护public override async Task StartAsync(CancellationToken cancellationToken) { try { await base.StartAsync(cancellationToken); await Task.Run(() InitializeDatabase(), cancellationToken); // ✅ 正确委托给线程池并传 token } catch (OperationCanceledException) { _logger.LogWarning(StartAsync cancelled.); throw; } }4.3 现象服务运行中Event Viewer → Windows Logs → Application里只有Information级别日志无Error或Warning原因appsettings.json中Logging配置覆盖了UseWindowsService()的默认 EventLog 设置。例如{ Logging: { LogLevel: { Default: Information }, Console: { LogLevel: { Default: None } } } }此配置会禁用所有 Provider包括 EventLog。解决显式启用 EventLog{ Logging: { LogLevel: { Default: Information }, EventLog: { LogLevel: { Default: Information } } } }4.4 现象卸载服务后sc delete MyWinService报错Access is denied但当前用户是 Administrator原因服务进程仍在内存中sc query显示STATE: 4 RUNNING或服务被其他进程如svchost.exe托管。常见于BackgroundService的StopAsync未正确 await导致Dispose未执行完就被 SCM 强制 kill。解决先强制停止Stop-Service MyWinService -Force检查进程Get-Process | Where-Object {$_.Path -like *MyWinService*} | Stop-Process -Force再删除Remove-Service MyWinServicePowerShell或sc delete MyWinServiceCMD。5. 生产级加固日志归档、自动恢复、配置热更新三大实战技巧5.1 日志不进 EventLog用 NLog File Target 实现双通道落盘UseWindowsService()提供的 EventLog 日志虽合规但容量有限默认 20MB、查询不便、无法按业务维度过滤。生产环境必须叠加文件日志。dotnet5-winservice-demo.zip采用 NLog因其支持AsyncWrapper和ArchiveAboveSize避免 I/O 阻塞主线程。安装包dotnet add package NLog.WindowsEventLog dotnet add package NLog.Extensions.Loggingnlog.config关键配置?xml version1.0 encodingutf-8 ? nlog xmlnshttp://www.nlog-project.org/schemas/NLog.xsd xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance autoReloadtrue internalLogLevelInfo internalLogFilelogs/internal-nlog.txt extensions add assemblyNLog.WindowsEventLog / /extensions targets !-- Windows EventLog -- target xsi:typeEventLog nameeventlog sourceMyWinService layout${longdate} ${level:uppercasetrue} ${logger} ${message} ${exception:formattostring} / !-- 文件日志自动归档 -- target xsi:typeFile namefile fileNamelogs/${shortdate}.log archiveFileNamelogs/archives/{#}.log archiveEveryDay archiveNumberingRolling maxArchiveFiles30 concurrentWritestrue keepFileOpenfalse encodingUTF-8 layout${longdate} ${level:uppercasetrue} ${logger} ${message} ${exception:formattostring} / /targets rules logger name* minlevelInfo writeToeventlog,file / /rules /nlog在Program.cs中注册public static IHostBuilder CreateHostBuilder(string[] args) Host.CreateDefaultBuilder(args) .UseWindowsService() .ConfigureLogging(logging { logging.ClearProviders(); // 清除默认 provider logging.SetMinimumLevel(LogLevel.Information); }) .UseNLog() // ← 加载 nlog.config .ConfigureServices((hostContext, services) { services.AddHostedServiceMyBackgroundService(); });技巧archiveEveryDaymaxArchiveFiles30实现日志自动轮转避免磁盘打满concurrentWritestrue允许多线程写入keepFileOpenfalse减少句柄泄漏——这是dotnet5-winservice-demo.zip在 200 QPS 场景下稳定运行 18 个月的关键配置。5.2 服务崩溃后自动重启SCM 恢复策略配置表Windows SCM 内置服务恢复机制无需代码干预。install.ps1中通过sc failure命令配置# 在 install.ps1 末尾追加 $servicePath sc failure $ServiceName reset 0 actions restart/60000/restart/60000/restart/60000 Invoke-Expression $servicePath Write-Host Recovery policy set: Restart on 1st/2nd failure after 60s, 3rd failure after 60s -ForegroundColor Yellow效果等同于 GUI 中服务属性 → 恢复 → 第一次失败重新启动服务第二次失败重新启动服务后续失败重新启动服务重置失败计数1 天重新启动服务60 秒此配置让服务具备基础韧性——即使BackgroundService因未捕获异常崩溃SCM 也会在 60 秒内拉起新进程业务中断时间 2 分钟。5.3 配置热更新监听appsettings.json变更并重载IConfigurationBackgroundService运行时IConfiguration默认不响应文件变更。需手动监听public class MyBackgroundService : BackgroundService { private readonly ILoggerMyBackgroundService _logger; private readonly IConfiguration _configuration; private IChangeToken _changeToken; public MyBackgroundService(ILoggerMyBackgroundService logger, IConfiguration configuration) { _logger logger; _configuration configuration; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // 首次加载配置 ReloadConfig(); // 监听配置变更 _changeToken _configuration.GetReloadToken(); _changeToken.RegisterChangeCallback(_ { _logger.LogInformation(Configuration changed. Reloading...); ReloadConfig(); }, null); while (!stoppingToken.IsCancellationRequested) { var interval _configuration.GetValueint(SyncIntervalSeconds, 300); _logger.LogInformation(Current sync interval: {Interval}s, interval); await Task.Delay(TimeSpan.FromSeconds(interval), stoppingToken); } } private void ReloadConfig() { // 重新读取配置项或触发下游服务重载 _configuration.Reload(); } }注意GetReloadToken()返回的IChangeToken是 .NET Core 3.0 的标准接口无需额外 NuGet 包。dotnet5-winservice-demo.zip中appsettings.json的SyncIntervalSeconds字段即可在线调整任务频率无需重启服务——这是交付给客户时最被赞赏的“免停机运维”能力。我带过的三个项目里有两次翻车都发生在UseWindowsService()调用位置错误一次栽在PublishTrimmed导致ServiceBase初始化失败。现在我的Program.cs模板第一行就是注释// ⚠️ UseWindowsService() MUST be BEFORE ConfigureServices()第二行是// ⚠️ Do NOT use PublishTrimmed with Windows Services unless tested。这些不是教条是拿客户服务器蓝屏换来的后悔药。希望帮到你。本文还有配套的精品资源点击获取