ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

.NET 5 Windows服务实战:从开发到部署的完整工程指南

.NET 5 Windows服务实战:从开发到部署的完整工程指南 简介本资源是一套基于.NET 5构建Windows服务的完整实践项目面向C#开发者、.NET后端工程师及希望掌握跨平台长期运行服务开发的技术人员解决传统Windows服务开发门槛高、配置复杂、日志与配置管理不统一等实际问题。压缩包为ZIP格式大小7.1MB包含服务宿主程序、log4net日志集成模块、appsettings.json配置读写示例、IHostedService托管服务实现、内置HTTP监听器及轻量API接口还整合了Ant Design Pro前端对接逻辑体现前后端协同部署思路。目前已有410人学习下载项目结构清晰涵盖从服务注册、生命周期管理、配置热更新到跨平台部署的关键环节附带可直接运行的完整源码与注释说明便于快速理解Worker Service在Windows服务场景下的工程化落地路径。1. 为什么用 .NET 5 写 Windows 服务不再是“玄学操作”而是可复现、可交付的工程实践你有没有遇到过交付一个后台任务客户非要它“开机自启、不依赖用户登录、能被服务管理器识别”——结果你写了个控制台程序加个计划任务半夜系统休眠后就停了或者用 TopShelf 包裹 .NET Core 3.1升级到 .NET 5 后发现ServiceBase的生命周期钩子行为突变日志打不出来、停止信号收不到、OnStart里开线程却卡死在ServiceController.Status StartPending……这不是配置错误是底层 Hosting 模型和 Windows SCMService Control Manager握手逻辑变了。.NET 5是首个将Microsoft.Extensions.Hosting与原生 Windows 服务宿主深度对齐的 LTS 版本它不再需要第三方包装层而是通过IHostBuilder.UseWindowsService()直接注入 SCM 生命周期语义。这个dotnet5-winservice-demo.zip不是玩具 demo而是一套经过 Windows Server 2012 R2 / Windows 10 1809 / Windows 11 多环境实测的最小可行交付包含服务注册、安装/卸载脚本、事件日志写入、优雅关闭、配置热重载、以及最关键的——服务启动失败时能准确定位是 SCM 权限问题、还是 Host 初始化异常、还是依赖注入容器构建失败。适合正在把旧版 Windows Forms 后台工具迁移到 .NET 5 的一线开发、需要交付稳定后台服务的 ISV 工程师以及被“错误 1053服务没有及时响应启动或控制请求”折磨到凌晨三点的运维同学。2. 从零构建一个真正能进 services.msc 的 .NET 5 Windows 服务2.1 创建项目结构避开模板陷阱用 CLI 手动初始化最稳Visual Studio 的“Windows Service (.NET Core)”模板默认生成的是基于ServiceBase的老式风格它绕过了 .NET 5 的通用主机模型导致无法使用IOptionsT、ILoggerT的 DI 注入也无法享受HostBuilder的配置链式构建能力。正确做法是用console模板起手再手动接入 Windows 服务宿主。# 创建标准控制台项目不是 Windows Service 模板 dotnet new console -n MyWinService -f net5.0 cd MyWinService # 添加必需 NuGet 包注意版本必须为 5.0.x不能用 6.0 dotnet add package Microsoft.Extensions.Hosting.WindowsServices --version 5.0.17 dotnet add package Microsoft.Extensions.Logging.EventLog --version 5.0.17提示Microsoft.Extensions.Hosting.WindowsServices是 .NET 5 中唯一官方支持 Windows 服务宿主的包它提供UseWindowsService()扩展方法。不要用TopShelf或WinSW它们在 .NET 5 中已失去维护且与IHostLifetime冲突。项目结构应为MyWinService/ ├── MyWinService.csproj ├── Program.cs # 主机入口 ├── Worker.cs # 业务逻辑继承 BackgroundService ├── appsettings.json # 配置文件支持 JSON 环境变量覆盖 └── appsettings.Production.json关键点在于服务本质仍是控制台应用只是运行时告诉 .NET Host 它要以 Windows 服务模式启动。这决定了后续所有调试、部署、日志路径都必须按此范式设计。2.2 编写 Program.csHostBuilder 的三段式初始化必须严格遵循顺序.NET 5 的 Windows 服务要求UseWindowsService()必须在Build()之前调用且必须在ConfigureServices()之后、ConfigureLogging()之前。顺序错一位服务就会静默失败SCM 显示“启动中”然后超时。// Program.cs using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace MyWinService { public class Program { public static void Main(string[] args) { // 1. 创建 HostBuilder必须用 CreateDefaultBuilder var host Host.CreateDefaultBuilder(args) // 2. 【关键】启用 Windows 服务宿主必须在此处 .UseWindowsService() // 3. 配置服务注册 Worker、Logger、配置源等 .ConfigureServices((hostContext, services) { // 注册后台工作服务核心业务逻辑 services.AddHostedServiceWorker(); // 注册配置自动加载 appsettings.json 和环境变量 services.ConfigureAppSettings(hostContext.Configuration.GetSection(AppSettings)); }) // 4. 配置日志必须显式添加 EventLog否则日志不进 Windows 事件查看器 .ConfigureLogging((hostContext, logging) { logging.ClearProviders(); // 清除默认 ConsoleProvider服务模式下无控制台 logging.AddEventLog(options { options.SourceName MyWinService; // 事件日志源名必须与安装时一致 options.LogName Application; // 日志位置建议用 Application 而非自定义 }); }); // 5. 构建并运行服务模式下 run() 会阻塞交由 SCM 管理生命周期 host.Build().Run(); } } }逻辑说明CreateDefaultBuilder()是唯一支持UseWindowsService()的构建器自定义HostBuilder会丢失 SCM 集成。UseWindowsService()内部做了三件事① 检测是否在服务上下文运行Environment.UserInteractive false② 替换默认IHostLifetime为WindowsServiceLifetime③ 注册ServiceBase实例并绑定OnStart/OnStop到IHostApplicationLifetime.StopApplication()。ClearProviders()是血泪经验不清理默认ConsoleLoggerProvider服务启动时会因找不到控制台句柄而抛InvalidOperationException但错误被 SCM 吞掉只显示“错误 1053”。2.3 实现 Worker.csBackgroundService 的生命周期必须与 SCM 信号对齐很多翻车案例源于直接在Worker.ExecuteAsync()里写while(true)死循环却不响应stoppingToken。SCM 发送停止信号时BackgroundService会调用StopAsync()并传入CancellationToken若你的业务逻辑没监听该 token服务就会卡在“停止中”状态最终被 SCM 强制终止导致数据丢失。// Worker.cs using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Threading; using System.Threading.Tasks; namespace MyWinService { public class Worker : BackgroundService { private readonly ILoggerWorker _logger; private readonly AppSettings _settings; public Worker(ILoggerWorker logger, IOptionsAppSettings settings) { _logger logger; _settings settings.Value; } // 【关键】ExecuteAsync 是服务主体必须用 stoppingToken 控制退出 protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation(MyWinService is starting.); try { while (!stoppingToken.IsCancellationRequested) { // 执行业务逻辑例如每 30 秒检查一次文件夹 await DoWorkAsync(stoppingToken); // 使用 Delay 而非 Sleep确保能响应 cancellation await Task.Delay(_settings.IntervalSeconds * 1000, stoppingToken); } } catch (OperationCanceledException) { // stoppingToken 触发的异常是正常流程不要记录为 Error _logger.LogInformation(MyWinService is stopping.); } catch (Exception ex) { _logger.LogError(ex, Error in MyWinService background task.); } } // 【关键】StopAsync 必须 await 所有异步清理操作 public override async Task StopAsync(CancellationToken stoppingToken) { _logger.LogInformation(MyWinService is stopping gracefully.); // 执行资源释放如关闭 HttpClient、保存缓存、提交事务 await CleanupAsync(stoppingToken); await base.StopAsync(stoppingToken); // 调用基类触发 HostApplicationLifetime.Stopped } private async Task DoWorkAsync(CancellationToken ct) { // 示例读取配置中的路径扫描文件 var files Directory.GetFiles(_settings.WatchPath, *.log, SearchOption.TopDirectoryOnly); _logger.LogInformation(Found {FileCount} log files., files.Length); } private async Task CleanupAsync(CancellationToken ct) { // 模拟异步清理如上传未完成日志 await Task.Delay(1000, ct); } } }参数说明stoppingToken是 SCM 停止命令的载体所有await操作必须传入它否则Task.Delay会忽略中断信号。base.StopAsync()必须最后调用它会触发IHostApplicationLifetime.ApplicationStopped事件供其他组件监听。_logger.LogInformation写入的事件会出现在 Windows 事件查看器 → 应用程序日志 → 来源为“MyWinService”的条目中这是排查启动失败的第一现场。3. 安装、启动、调试让服务真正进入 services.msc 的三步闭环3.1 编译输出发布为 self-contained 单文件避免目标机器缺 runtime.NET 5 Windows 服务必须以self-contained方式发布否则客户服务器上若未安装 .NET 5 Runtime服务会直接报“找不到指定模块”错误 1053 的常见原因。不要用framework-dependent。# 在项目根目录执行注意-r 参数指定运行时标识符 dotnet publish -c Release -r win-x64 --self-contained true -o ./publish # 输出目录结构 # publish/ # ├── MyWinService.exe # 可执行文件含 runtime # ├── MyWinService.dll # ├── appsettings.json # └── ...所有依赖 DLL提示win-x64是 Windows Server 2012 / Windows 10 的标准运行时。若需支持 Windows 7改用win-x64--no-self-contained但必须提前在目标机安装 .NET 5 Desktop Runtime。3.2 安装服务用 sc.exe 命令行注册拒绝图形化安装工具很多团队用 PowerShell 脚本或第三方 installer结果权限不足、路径含空格、描述乱码。最稳的方式是sc.exe—— Windows 原生命令无需额外依赖且错误信息直指根源。:: 以管理员身份打开 CMD右键 → “以管理员身份运行” :: 进入 publish 目录 cd C:\path\to\publish :: 创建服务关键参数说明见下表 sc create MyWinService binPath C:\path\to\publish\MyWinService.exe start auto obj NT AUTHORITY\LocalService DisplayName MyWinService - 文件监控服务 :: 启动服务 sc start MyWinService :: 查看状态确认 State 为 4 RUNNING sc query MyWinService参数值说明binPath绝对路径\MyWinService.exe必须用双引号包裹路径否则空格导致解析失败错误 1053startauto开机自启demand为手动启动disabled禁用objNT AUTHORITY\LocalService推荐用LocalService最低权限避免用System或域账户需密码DisplayNameMyWinService - 描述services.msc 中显示的名称支持中文注意sc create命令中后面必须紧跟值中间不能有空格如start auto错startauto对。这是 Windows 服务注册的硬语法。3.3 调试技巧在非服务模式下运行快速验证逻辑每次改代码都要sc stop/start太慢用--windows-service参数开关控制运行模式// Program.cs 中修改 Main 方法 public static void Main(string[] args) { // 检测是否传入 --windows-service 参数 var isService !(Debugger.IsAttached || args.Contains(--console)); var host Host.CreateDefaultBuilder(args) .ConfigureServices(services { services.AddHostedServiceWorker(); }); if (isService) { host.UseWindowsService(); // 仅在服务模式启用 } host.Build().Run(); }然后正常调试dotnet run→ 启动为控制台程序断点、日志全可见模拟服务dotnet run -- --windows-service→ 模拟 SCM 环境但仍在控制台输出日志真机测试sc start→ 最终验证这样就把开发调试和生产部署彻底解耦避免“本地跑得通装上去就 1053”。4. 避坑指南那些让 .NET 5 Windows 服务启动失败的 5 个真实场景4.1 现象服务状态卡在 START_PENDING10 秒后报“错误 1053服务没有及时响应启动或控制请求”原因Program.Main()中Host.Build().Run()被阻塞在某个同步 IO 或死锁或Worker.ExecuteAsync()未正确使用stoppingToken导致Task.Delay不响应中断。解决在ExecuteAsync开头加_logger.LogInformation(Worker started);若日志没出现说明卡在Host.Build()阶段检查ConfigureServices中是否有同步HttpClient.Send()、File.ReadAllText()等阻塞调用全部改为async/await确保Task.Delay传入stoppingToken而非CancellationToken.None。4.2 现象服务启动成功但事件查看器中无日志sc query显示状态为 RUNNING 却无实际工作原因logging.AddEventLog()未配置SourceName或SourceName与sc create时的DisplayName不一致导致日志写入失败静默丢弃。解决运行eventvwr.msc→ Windows 日志 → 应用程序 → 右键“筛选当前日志” → 按“来源”筛选“MyWinService”若无记录检查appsettings.json中是否误删了Logging: { LogLevel: { Default: Information } }确认AddEventLog(options options.SourceName MyWinService)与sc create的DisplayName名称完全一致大小写敏感。4.3 现象服务安装后无法启动sc queryex显示WIN32_EXIT_CODE: 1067原因binPath指向的.exe文件不存在或路径含中文/空格未用双引号包裹或obj指定的账户无“作为服务登录”权限。解决运行sc qc MyWinService查看BINARY_PATH_NAME是否正确手动执行该路径的.exe观察是否弹出“找不到 dll”错误说明未self-contained发布用secpol.msc→ 本地策略 → 用户权限分配 → “作为服务登录”确认LocalService已加入。4.4 现象服务能启动但appsettings.json中的配置项读取为空IOptionsAppSettings注入失败原因ConfigureServices中services.ConfigureAppSettings(...)的 section 名称与appsettings.json中实际 section 名不匹配或appsettings.json未设为“复制到输出目录”。解决检查appsettings.json结构是否为{ AppSettings: { IntervalSeconds: 30, WatchPath: C:\\Logs } }在csproj中确认Content Includeappsettings.json CopyToOutputDirectoryPreserveNewest/CopyToOutputDirectory /Content在Worker构造函数中加_logger.LogInformation(Config loaded: {Interval}, _settings.IntervalSeconds);验证。4.5 现象服务卸载后sc query仍显示服务存在再次安装报“发生系统错误 5拒绝访问”原因sc delete未以管理员权限执行或服务进程残留SCM 未完全释放句柄。解决用tasklist /svc | findstr MyWinService查看是否有残留进程用taskkill /f /im MyWinService.exe强制结束用regedit进入HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MyWinService手动删除该 key高危操作仅当 sc delete 失败时用永远用sc delete MyWinService而非图形化工具卸载确保 SCM 元数据清理干净。5. 生产级加固日志归档、配置热重载、服务依赖与故障自愈5.1 日志归档用 NLog 替代 EventLog实现按大小轮转 压缩Windows EventLog 不支持自动归档和压缩日志文件超过 1GB 就变卡。换成NLog可精准控制!-- 在 csproj 中添加 -- PackageReference IncludeNLog Version4.7.15 / PackageReference IncludeNLog.Extensions.Logging Version1.7.4 /// Program.cs 中替换 ConfigureLogging .ConfigureLogging((hostContext, logging) { logging.ClearProviders(); logging.SetMinimumLevel(LogLevel.Information); logging.AddNLog(new NLogProviderOptions { CaptureMessageTemplates true, CaptureMessageProperties true }); })!-- nlog.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 targets target xsi:typeFile namefile fileNamelogs/${shortdate}.log layout${longdate}|${level:uppercasetrue}|${logger}|${message} ${exception:formattostring} archiveFileNamelogs/archived.{#}.log archiveEveryDay archiveNumberingRolling archiveAboveSize10485760 !-- 10MB -- maxArchiveFiles30 enableArchiveCompresiontrue / /targets rules logger name* minlevelInfo writeTofile / /rules /nlog效果日志按天分割单个文件超 10MB 自动归档为archived.1.log.gz保留 30 天内部错误写入internal-nlog.txt用于诊断 NLog 自身问题。5.2 配置热重载监听 appsettings.json 变更无需重启服务.NET 5 原生支持IOptionsMonitorT但需确保appsettings.json设置为CopyToOutputDirectory且reloadOnChangetrue// Program.cs 中 ConfigureServices services.ConfigureAppSettings(hostContext.Configuration.GetSection(AppSettings)) .AddOptionsAppSettings() .Bind(hostContext.Configuration.GetSection(AppSettings)) .ValidateDataAnnotations(); // 启用数据注解校验 // Worker.cs 中注入 IOptionsMonitor private readonly IOptionsMonitorAppSettings _settingsMonitor; public Worker(ILoggerWorker logger, IOptionsMonitorAppSettings settingsMonitor) { _logger logger; _settingsMonitor settingsMonitor; _settingsMonitor.OnChange(ReloadSettings); // 订阅变更 } private void ReloadSettings(AppSettings settings, string _) { _logger.LogInformation(Configuration reloaded: Interval{Interval}s, settings.IntervalSeconds); // 这里可触发业务逻辑重配置如调整 Timer 间隔 }注意appsettings.json的CopyToOutputDirectory必须设为PreserveNewest且reloadOnChange默认为true.NET 5无需额外代码。5.3 服务依赖声明依赖项确保服务按序启动若你的服务依赖Base Filtering EngineBFE或Windows Management InstrumentationWMI必须显式声明否则 SCM 可能先启动你的服务再启动依赖项导致连接失败:: 在安装后执行以管理员身份 sc config MyWinService depend bfe/wmidepend后跟依赖服务的SERVICE_NAME非 DisplayName多个用/分隔。可通过sc queryex bfe查看 BFE 的 SERVICE_NAME。5.4 故障自愈进程崩溃后自动重启避免人工干预Windows 服务本身不提供崩溃重启需配置恢复策略:: 设置服务崩溃后1 分钟内重启第二次失败后 5 分钟重启第三次失败后运行脚本 sc failure MyWinService reset 86400 actions restart/60000/restart/300000/run/300000 :: 指定崩溃后运行的脚本例如发邮件告警 sc failureflag MyWinService 1reset86400表示 24 小时计数周期actions中三个动作对应三次失败run/300000表示第三次失败后 5 分钟运行C:\path\to\alert.bat。我在线上环境吃过亏某次HttpClientDNS 解析超时未设CancellationToken导致ExecuteAsync卡死SCM 以为服务假死反复重启却没日志。后来强制所有网络调用加CancellationToken并在catch (OperationCanceledException)中记录“任务被取消”才真正把“服务不可用”变成“可定位、可修复”的事件。这套 .NET 5 Windows 服务方案我们已交付给 7 家制造业客户最长稳定运行 412 天无重启。它不炫技但每一步都踩在 Windows 服务的真实约束上——SCM 的超时机制、LocalService 的权限边界、EventLog 的写入限制、以及开发者最需要的失败时错误在哪一行代码而不是“错误 1053”四个字。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进