ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

.NET 5 Windows服务开发实战:从IHostedService到sc部署

.NET 5 Windows服务开发实战:从IHostedService到sc部署 简介本资源是一套基于.NET 5开发Windows服务的完整实践项目面向C#开发者、后端工程师及.NET技术进阶学习者解决传统Windows服务在跨平台、日志集成、配置管理与HTTP托管等方面的开发痛点。项目采用Worker Service模板构建涵盖服务注册与生命周期管理、log4net日志集成、appsettings.json配置读写、后台任务托管、内建Kestrel HTTP监听服务及Ant Design Pro前端对接等核心能力适用于企业级后台服务、运维工具或轻量API网关场景。压缩包为ZIP格式大小7.1MB包含源码工程、配置文件、依赖说明及启动脚本等典型.NET项目结构文件无冗余资源结构清晰便于快速上手与二次开发。目前已有410人学习下载读者可直接获取可运行的完整解决方案含服务安装/卸载脚本、日志分级输出示例、配置热更新逻辑及前后端联调参考显著降低Windows服务开发门槛。1. 用 .NET 5 写 Windows 服务不是“控制台加个 InstallUtil”就完事而是要绕过 Session 0 隔离、ServiceControlManager 权限校验、以及 .NET Core 运行时加载黑匣子的完整链路你手头刚接到一个需求把一段跑在后台的设备心跳检测逻辑从原来手动双击的控制台程序改成开机自启、崩溃自动重启、日志可被 Windows 事件查看器捕获的真正 Windows 服务。你搜“.NET Windows 服务”前几页全是用InstallUtil.exe注册.exe的老方案——但那是 .NET Framework 时代的遗物。当你切到 .NET 5或更高版本dotnet publish出来的不是.exe而是带.deps.json、.runtimeconfig.json和一堆.dll的文件夹InstallUtil根本不认识这种结构强行注册会报错Could not load file or assembly System.ServiceProcess.ServiceController。更糟的是即使你靠sc create手动注册成功服务启动时大概率卡在Starting...状态Windows 事件日志里只有一句模糊的Error 1053: The service did not respond to the start or control request in a timely fashion。这不是代码写错了是 .NET 5 的 HostBuilder 没正确接管 ServiceControlManager 的生命周期钩子是ServiceBase.Run()被错误地放在了Main入口里是ServiceProcessInstaller类在 .NET 5 中已被标记为[Obsolete]却没人告诉你该用什么替代。这份dotnet5-winservice-demo.zip就是为解决这整条链路上的断点而生它不依赖任何第三方 NuGet 包如Microsoft.Extensions.Hosting.WindowsServices的旧版兼容层所有代码基于 .NET 5 SDK 原生能力包含完整的Program.cs启动模板、MyWinService.cs服务主体、appsettings.json配置注入示例以及最关键的——一份能直接sc createsc start成功、且能在 Windows Server 2012 R2 到 Windows 11 全系系统上稳定运行的.bat部署脚本。适合正在交付工业网关采集服务、IoT 设备管理后台、或需要长期驻留本地的 .NET 数据同步模块的工程师。2. 为什么必须用 IHostedService WindowsServiceLifetime拆解 .NET 5 Windows 服务的三层启动模型2.1 传统 .NET Framework 服务模型已失效Session 0 隔离与 SCM 权限变更的底层约束在 Windows Vista 之后微软强制推行 Session 0 隔离机制所有 Windows 服务必须运行在 Session 0无桌面交互权限而用户登录后默认进入 Session 1。这意味着任何试图在服务中弹出 MessageBox、操作剪贴板、或调用ShellExecute打开浏览器的操作都会静默失败。而 .NET Framework 时代的ServiceBase模型其OnStart方法本质是向 SCMService Control Manager注册一个回调函数SCM 在收到START请求后会以LocalSystem或指定账户身份在 Session 0 中调用该回调。这个模型在 .NET 5 中无法直接复用因为ServiceBase类虽仍存在但其内部依赖的System.ServiceProcess.dll在 .NET Core/.NET 5 中被大幅精简关键方法如ServiceBase.Run()的实现已不再兼容新的托管环境。更致命的是SCM 对服务可执行文件的签名和清单要求变严格它要求服务主程序必须导出ServiceMain入口点C/C 风格而 .NET 5 的dotnet.exe是通用宿主不满足此条件。因此硬套InstallUtil会失败不是因为命令不对而是因为整个架构层级已断裂。2.2 .NET 5 的正确路径IHostedService 接口 WindowsServiceLifetime 的组合拳.NET 5 引入了Microsoft.Extensions.Hosting.WindowsServices官方包注意不是Microsoft.AspNetCore.Hosting.WindowsServices后者已废弃它提供了一个轻量级适配层将标准的IHostedService生命周期StartAsync/StopAsync桥接到 Windows SCM 的SERVICE_STATUS状态机。其核心在于WindowsServiceLifetime类——它继承自BackgroundService并在构造时通过 P/Invoke 调用SetServiceStatus向 SCM 报告服务状态SERVICE_START_PENDING,SERVICE_RUNNING等。当 SCM 发送SERVICE_CONTROL_START控制码时WindowsServiceLifetime的StartAsync方法被触发发送SERVICE_CONTROL_STOP时StopAsync被触发。这完全绕过了ServiceBase的陈旧机制让服务逻辑可以完全基于现代 .NET 的 DI 容器、配置系统和日志框架构建。dotnet5-winservice-demo.zip中的Program.cs正是采用此模式// Program.cs - .NET 5 Windows 服务标准启动模板 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; var host Host.CreateDefaultBuilder(args) .UseWindowsService() // 关键启用 WindowsServiceLifetime 适配器 .ConfigureServices((context, services) { services.AddHostedServiceMyWinService(); // 注册你的业务服务 services.ConfigureHostOptions(options { options.ServicesStartTimeout TimeSpan.FromSeconds(30); // 防止 Error 1053 }); }) .Build(); await host.RunAsync();提示UseWindowsService()必须在ConfigureServices之前调用否则WindowsServiceLifetime不会被注册进 DI 容器。这是官方文档未明确强调但极易踩坑的顺序问题。2.3 服务主体 MyWinService.cs如何让心跳检测逻辑真正“活”在 Session 0MyWinService.cs是业务逻辑的载体它必须继承BackgroundService而非ServiceBase并重写ExecuteAsync方法。该方法会在服务启动后被持续调用直到服务停止。关键点在于所有耗时操作必须异步化且不能阻塞线程池。例如设备心跳检测若使用Thread.Sleep(30000)会导致ExecuteAsync无法及时返回SCM 认为服务未响应最终触发 Error 1053。正确做法是使用await Task.Delay// MyWinService.cs - 示例心跳检测服务 public class MyWinService : BackgroundService { private readonly ILoggerMyWinService _logger; private readonly IConfiguration _config; public MyWinService(ILoggerMyWinService logger, IConfiguration config) { _logger logger; _config config; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation(MyWinService is starting.); // 从 appsettings.json 读取设备地址 var deviceIp _config[Device:IpAddress] ?? 127.0.0.1; var intervalMs int.Parse(_config[Device:CheckIntervalMs] ?? 30000); while (!stoppingToken.IsCancellationRequested) { try { // 模拟设备心跳检测真实场景可能是 TCP 连接或 HTTP GET var isAlive await PingDeviceAsync(deviceIp, stoppingToken); _logger.LogInformation($Device {deviceIp} status: {(isAlive ? UP : DOWN)}); // 使用 await Task.Delay而非 Thread.Sleep await Task.Delay(intervalMs, stoppingToken); } catch (OperationCanceledException) { // stoppingToken 触发正常退出 break; } catch (Exception ex) { _logger.LogError(ex, Error in device ping loop); // 错误后仍继续循环避免服务意外退出 await Task.Delay(5000, stoppingToken); } } _logger.LogInformation(MyWinService is stopping.); } private async Taskbool PingDeviceAsync(string ip, CancellationToken ct) { // 真实项目中替换为实际检测逻辑 using var client new HttpClient(); try { var response await client.GetAsync($http://{ip}:8080/health, ct); return response.IsSuccessStatusCode; } catch { return false; } } }这段代码的关键设计CancellationToken全链路传递从ExecuteAsync到Task.Delay到HttpClient.GetAsync确保服务停止信号能穿透所有异步操作。错误隔离try/catch捕获所有异常记录日志后继续下一轮循环防止单次网络抖动导致整个服务崩溃退出。配置驱动IP 地址和检测间隔从appsettings.json读取便于部署时修改无需重新编译。3. 从源码到可部署dotnet publish的参数陷阱与sc create的权限真相3.1dotnet publish必须指定--self-contained和--runtime否则服务启动即报错.NET 5应用有两种发布模式framework-dependent依赖目标机器安装 .NET Runtime和self-contained自带运行时。对于 Windows 服务必须选择self-contained。原因有二SCM 启动服务时不会为你设置PATH或DOTNET_ROOT环境变量。如果发布为 framework-dependent服务进程启动后找不到dotnet.exe会直接退出事件日志中仅显示The service process could not connect to the service controller.。不同 Windows 版本预装的 .NET Runtime 版本不一致。例如 Windows Server 2012 R2 默认无 .NET 5而 Windows 11 可能预装 .NET 6。self-contained发布可保证运行时版本完全可控。dotnet5-winservice-demo.zip中的publish.bat脚本明确指定了参数echo off setlocal :: 清理旧发布目录 if exist publish\ rmdir /s /q publish\ :: 发布为自包含应用目标运行时 win-x64 dotnet publish -c Release -r win-x64 --self-contained true -p:PublishTrimmedfalse -o publish\ echo Publish completed. Output in publish\ folder. pause注意-r win-x64中的win-x64是 RIDRuntime Identifier必须与目标服务器 CPU 架构严格匹配。若部署到 32 位 Windows极罕见需改为win-x86若为 ARM64 服务器如 Windows on ARM则用win-arm64。-p:PublishTrimmedfalse禁用裁剪避免因反射调用丢失类型导致运行时异常。3.2sc create命令的四个必填参数binPath、start、obj、DisplayNamesc create是注册服务的唯一可靠方式InstallUtil已淘汰。其语法看似简单但每个参数都有玄学细节sc create MyWinServiceDemo ^ binPath C:\MyService\publish\MyWinServiceDemo.exe ^ start auto ^ obj NT AUTHORITY\LocalService ^ DisplayName My WinService Demo ^ depend TcpipbinPath必须是完整绝对路径且路径中不能有空格若有需用引号包裹整个值如上例。路径末尾的.exe不能省略即使publish输出的是.dll主程序dotnet publish -r win-x64会生成真正的.exe宿主。startauto表示开机自启demand表示手动启动disabled表示禁用。auto是最常用选项。obj服务运行账户。NT AUTHORITY\LocalService权限最低仅能访问本机资源适合绝大多数场景NT AUTHORITY\NetworkService可访问网络.\Administrator等具体账户需提前设置密码不推荐。depend服务依赖项。Tcpip表示依赖 TCP/IP 协议栈确保网络可用后再启动服务。若服务需访问数据库可添加MSSQLSERVERSQL Server 服务名。提示sc create命令必须以管理员权限运行。右键点击 CMD 或 PowerShell选择“以管理员身份运行”否则会报错Access is denied。3.3 验证服务注册与启动sc query与事件查看器的黄金组合服务创建后不能只信sc create的成功提示。必须用sc query确认状态并用 Windows 事件查看器定位深层错误:: 查询服务状态 sc query MyWinServiceDemo :: 查看最近 10 条服务相关事件Application 日志 wevtutil qe Application /q:*[System[(EventID7036 or EventID7040) and Provider[NameService Control Manager]]] /c:10 /rd:true /f:textsc query返回的STATE字段是关键4 RUNNING服务已成功启动。1 STOPPED服务已停止但注册成功。2 START_PENDING或3 STOP_PENDING服务卡在启动/停止过程中此时必须查事件日志。事件查看器中重点筛选Application日志下的Service Control Manager事件Event ID 7036服务状态变更如running,stopped。Event ID 7040服务启动失败消息中会明确写出错误代码如Error 1053和失败原因如The service process could not connect to the service controller.。4. 避坑五个血泪经验总结的常见问题与排查指南4.1 现象服务状态始终为START_PENDING10 秒后自动变为STOPPED事件日志报Error 1053原因ExecuteAsync方法未在ServicesStartTimeout默认 15 秒内完成首次迭代SCM 认为服务未响应。常见于MyWinService.cs中ExecuteAsync内部有同步阻塞操作如Thread.Sleep、File.ReadAllText读大文件、或HttpClient初始化超时。解决在Program.cs的ConfigureServices中显式延长超时services.ConfigureHostOptions(options options.ServicesStartTimeout TimeSpan.FromSeconds(60));确保ExecuteAsync内所有耗时操作都await并传入stoppingToken。若需初始化耗时操作如连接数据库应在StartAsync中完成而非ExecuteAsync开头。4.2 现象服务启动后立即退出sc query显示STOPPED事件日志无任何Service Control Manager相关事件原因dotnet publish未使用--self-contained true或binPath指向了framework-dependent发布的.dll文件如MyWinServiceDemo.dll而非.exe文件。SCM 尝试执行.dll失败进程静默退出。解决进入publish目录确认存在MyWinServiceDemo.exe文件而非只有.dll。用记事本打开MyWinServiceDemo.exe若开头是MZ字符PE 文件头说明是正确生成的宿主若开头是纯文本如{runtimeOptions:{...}}则是.runtimeconfig.json证明发布错误。重新执行dotnet publish -r win-x64 --self-contained true。4.3 现象服务能启动但ILogger日志不写入 Windows 事件查看器appsettings.json配置不生效原因UseWindowsService()必须在ConfigureServices之前调用否则WindowsServiceLifetime未被注册服务启动时未正确初始化IHostEnvironment和IConfiguration。解决检查Program.cs中UseWindowsService()是否位于ConfigureServices之前。确保appsettings.json文件的Copy to Output Directory属性设为Copy alwaysVisual Studio 中右键文件 → 属性否则发布后缺失配置文件。在MyWinService.cs构造函数中添加_logger.LogInformation($Config Device:IpAddress {_config[Device:IpAddress]})验证配置是否加载成功。4.4 现象服务在 Windows Server 2012 R2 上启动失败事件日志报Error 1000描述为Faulting application name: MyWinServiceDemo.exe原因win-x64RID 发布的二进制文件其最低 Windows 版本要求为 Windows 10 / Windows Server 2016。Windows Server 2012 R2 需要win81-x64或win7-x64RID。解决修改publish.bat将-r win-x64改为-r win7-x64支持 Windows 7 SP1 及以上覆盖 2012 R2。重新发布并部署。注意win7-x64发布包体积略大但兼容性最佳。4.5 现象服务启动后MyWinService.cs中的ExecuteAsync从未执行_logger.LogInformation无任何输出原因MyWinService未被正确注册为IHostedService。常见于ConfigureServices中漏掉services.AddHostedServiceMyWinService()或MyWinService类未继承BackgroundService。解决检查MyWinService.cs是否public class MyWinService : BackgroundService。检查Program.cs的ConfigureServices是否包含services.AddHostedServiceMyWinService()。在Program.cs的Build()之后、RunAsync()之前添加临时日志Console.WriteLine(Host built. Services count: host.Services.GetServicesIHostedService().Count());若输出为0证明注册失败。5. 日志、调试与生产部署让服务在无人值守环境下真正“可运维”5.1 将 ILogger 输出重定向到 Windows 事件日志告别黑盒运行默认情况下.NET 5的ILogger仅输出到控制台Console Logger。在 Windows 服务中控制台不可见必须将日志落地到 Windows 事件查看器。dotnet5-winservice-demo.zip已集成Microsoft.Extensions.Logging.EventLog包只需在Program.cs中启用var host Host.CreateDefaultBuilder(args) .UseWindowsService() .ConfigureLogging((context, logging) { // 移除默认的 Console Logger logging.ClearProviders(); // 添加 EventLog Logger logging.AddEventLog(new EventLogSettings { SourceName MyWinServiceDemo, // 事件源名称需与 sc create 的 DisplayName 一致 LogName Application // 写入 Application 日志 }); }) .ConfigureServices((context, services) { services.AddHostedServiceMyWinService(); }) .Build();注意首次运行时SourceName“MyWinServiceDemo” 需要在 Windows 注册表中创建。AddEventLog会自动尝试创建但需管理员权限。若失败可手动创建以管理员身份运行eventcreate /ID 1 /L APPLICATION /SO MyWinServiceDemo /T INFORMATION /D Service log source。5.2 服务内调试技巧如何在 Session 0 中 attach Visual Studio开发阶段你无法像控制台程序那样直接 F5 调试服务。但可通过以下步骤实现启动服务sc start MyWinServiceDemo。查找进程 PIDtasklist /svc | findstr MyWinServiceDemo获取 PID如1234。Attach 到进程在 Visual Studio 中Debug → Attach to Process勾选Show processes from all users找到MyWinServiceDemo.exePID 为1234点击Attach。设置断点在MyWinService.cs的ExecuteAsync或StartAsync中设置断点等待触发。提示若Attach后断点为灰色未命中检查 Visual Studio 的调试引擎是否选择了.NET Core而非Managed (CoreCLR)或Native。在Attach to Process对话框底部Attach to:下拉框应为Automatic: Native code或Managed (.NET Core)。5.3 生产环境部署 checklist一份防翻车的核对表检查项操作验证方式1. 发布完整性运行publish.bat确认publish\目录下存在.exe,.dll,.deps.json,.runtimeconfig.jsondir publish\ /b应列出至少 4 类文件2. 服务注册以管理员身份运行sc create命令sc query MyWinServiceDemo返回STATE: 1 STOPPED3. 依赖服务sc query Tcpip确认 TCP/IP 服务状态为RUNNING若为STOPPED需先sc start Tcpip4. 运行账户权限sc qc MyWinServiceDemo查看OBJECT_NAME确认为LocalService或NetworkService避免使用LocalSystem权限过高或具体用户需密码5. 首次启动sc start MyWinServiceDemo等待 30 秒sc query状态应为4 RUNNING事件查看器Application日志中出现Event ID 7036状态为running6. 业务日志在事件查看器中筛选Source: MyWinServiceDemo应看到MyWinService is starting.等日志5.4 服务更新与回滚零停机部署的实践脚本生产环境不能简单sc stop 替换文件 sc start这会导致服务中断。dotnet5-winservice-demo.zip提供了update-service.bat实现平滑更新echo off setlocal set SERVICE_NAMEMyWinServiceDemo set PUBLISH_DIRpublish\ set SERVICE_DIRC:\MyService\ :: 1. 停止当前服务 sc stop %SERVICE_NAME% timeout /t 10 /nobreak nul :: 2. 备份旧版本保留最近3个 if exist %SERVICE_DIR%backup\1\ rmdir /s /q %SERVICE_DIR%backup\3 if exist %SERVICE_DIR%backup\0\ move %SERVICE_DIR%backup\0 %SERVICE_DIR%backup\1 if exist %SERVICE_DIR% move %SERVICE_DIR% %SERVICE_DIR%backup\0 :: 3. 复制新版本 xcopy /E /I /Y %PUBLISH_DIR% %SERVICE_DIR% :: 4. 更新 binPathsc config sc config %SERVICE_NAME% binPath %SERVICE_DIR%MyWinServiceDemo.exe :: 5. 启动服务 sc start %SERVICE_NAME% echo Update completed. pause此脚本的核心思想是先停服再备份旧版最后更新binPath并启动。sc config修改binPath不影响已运行的服务只对下次启动生效因此sc start会加载新版本。备份机制确保 3 次更新内可快速回滚。从那以后我每次交付 Windows 服务都强制走一遍这个 checklist先publish看文件再sc create看注册然后sc startsc query 事件查看器三连查最后用update-service.bat模拟一次更新。这套流程让我在客户现场面对 Windows Server 2012 R2、2016、2019 的各种组合时再没遇到过“服务启动不了”的紧急电话。希望帮到你。本文还有配套的精品资源点击获取
返回列表