尧图精选

.NET5写Windows服务:Worker Service与部署避坑实战指南

🕒 发布时间:2026/10/1 6:11:55 📁 来源:尧图网络
简介一份基于 .NET 5 与 .NET 6 的 Windows 服务开发完整 Demo面向需要编写 Windows 服务或 Worker Service 的 .NET 开发人员可解决跨平台长期运行服务的托管、配置与接口开放等实际问题。工程打包为 zip 格式约 7.1MB体积精简便于下载后直接对照工程结构学习。代码覆盖服务基础配置、log4net 日志集成、配置文件读写、托管服务启动、HTTP 请求监听与对外接口并集成 Ant Design Pro 管理端方案有助于读者理解从后端服务到前端展示的完整链路。目前已有 410 人学习下载该 Demo 提供清晰的模块划分和关键实现思路适合希望参考现成代码、快速搭建 Windows 服务项目的开发者。1. 用 .NET5 写 Windows 服务这套 demo 把门槛压到了最低做后端的人迟早要碰 Windows 服务比如定时对账、MQ 队列消费、文件监听落库。我第一次用 .NET Framework 写服务时被坑得很惨要装 Installer 类、要做安装工程、调试还要跑到服务控制管理器里面去附加进程。后来换 .NET5 写发现事情简单得多——本质就是 Console 程序套一个BackgroundService服务化只是宿主多注册一个东西而已。这套 dotnet5-winservice-demo 就是把「能跑的 Worker 服务注册 安装卸载脚本 日志落盘」整个闭环给齐了适合两类人一是急着在 Windows Server 上落一个常驻后台任务的二是想弄明白 .NET5/6 的 Worker Service 到底怎么变成系统里那个服务的。下面从项目结构开始一步一步把它拆透。2. Worker Service 模板与 UseWindowsService先让程序能活2.1 Worker Service 与 Windows 服务的真实关系打开 Visual Studio 模板里的 Worker Service你会发现它就是个带泛型Host的控制台程序。所谓 Windows 服务本质上也是一个可执行文件只不过它没有界面由 SCM服务控制管理器去启动它、看护它。.NET5 以前要写服务必须走ServiceBase那套老代码.NET5 开始Generic Host里直接集成UseWindowsService等于把服务生命周期接进了现代 .NET 的依赖注入框架里。这个 demo 的骨干就是这个结构WorkerServiceDemo/ ├── Program.cs // 入口配置宿主 ├── Workers/ │ └── DemoWorker.cs // 继承 BackgroundService ├── Services/ │ └── JobService.cs // 业务逻辑从构造器注入 ├── ProjectInstaller.cs // 安装器给 installutil 用 ├── worker-service-demo.csproj └── appsettings.json我拆这个 demo 时先看的不是代码是 csproj。因为项目能不能跑成服务有一半在项目文件里就定了。常见做法是确认两个关键项PropertyGroup OutputTypeExe/OutputType TargetFrameworknet5.0/TargetFramework !-- 是否允许在 Windows 服务中运行的关键开关 -- UseWindowsForms/UseWindowsForms /PropertyGroup ItemGroup PackageReference IncludeMicrosoft.Extensions.Hosting.WindowsServices Version5.0.0 / /ItemGroupMicrosoft.Extensions.Hosting.WindowsServices这个包是关键它是 Windows 服务的宿主支持包。没有它即使代码里调用UseWindowsService()也编译不过。TargetFramework 如果你是 .NET6改成net6.0再加对应 6.x 版本的包就行代码不用动。这包做的事情很实在把主机的生存周期绑定到 SCM 上系统关机、服务停止时能触发IHost的优雅停止回调。2.2 Program.cs服务的心脏在这里切换Program.cs 是整套模板里最重要的文件务必逐行看。public class Program { public static void Main(string[] args) { IHost host Host.CreateDefaultBuilder(args) .UseWindowsService(options { // 服务列表中显示的名字区别于进程名 options.ServiceName DemoWorkerService; }) .ConfigureServices((context, services) { // 注册业务服务文件处理、HTTP 请求、仓库等 services.AddSingletonJobService(); // 注册后台任务 services.AddHostedServiceDemoWorker(); }) .Build(); host.Run(); } }这里的UseWindowsService是分水岭。代码逻辑和普通 Worker 完全一样但这行决定了它是否去响应 SCM 的控制指令。运行机制是当进程是被服务管理器启动的它就调用服务逻辑如果是从命令行直接dotnet WorkerServiceDemo.dll跑它照样以控制台模式跑方便调试。ServiceName不等同于可执行文件名它是服务管理器里注册的名称卸载、启动、停止都要用它。这个 demo 里叫DemoWorkerService你自己写新项目时记得换掉否则和你机器上已有的服务重名注册时直接报错。2.3 ExecuteAsync 的写法别把服务写成循环死等Worker 是任务的心脏demo 里的 DemoWorker 继承了BackgroundService核心是ExecuteAsync。protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // 服务刚启动时最怕构造函数或这里抛异常 _logger.LogInformation(Worker started at: {time}, DateTimeOffset.Now); while (!stoppingToken.IsCancellationRequested) { try { await _jobService.ProcessOnceAsync(stoppingToken); } catch (Exception ex) { // 单次任务失败不能把整个服务拖垮 _logger.LogError(ex, Task execution failed); } // 轮询间隔不要用 Thread.Sleep await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); } }三个地方容易出事。第一Task.Delay必须把stoppingToken传进去否则停止服务时工作线程会一直睡到自然醒SCM 等不到进程退出就会报服务停止超时或者直接杀进程。第二循环里的任务要捕获异常单次任务挂掉服务不应该跟着退出日志记下异常继续下一轮。第三ProcessOnceAsync里如果调外部接口建议自己加超时控制因为个别 HTTP 客户端默认超时是 100 秒服务被 SCM 认为无响应时往往就是卡在这种地方。提示stoppingToken在OnStopping触发后进入取消状态Task.Delay会立刻抛出OperationCanceledException。如果没传给 Delay系统停止服务时最坏要等满 30 秒。3. 把项目变成真正服务installutil、sc.exe 与自启动参数3.1 ProjectInstaller给 installutil 走的传统路径demo 里带了一个ProjectInstaller.cs这是从 .NET Framework 时代过来的老套路。它本质是用安装器类向系统注册服务名、描述、启动类型、运行账户。编译出来后在项目目录下执行installutil.exe WorkerServiceDemo.exeProjectInstaller里的核心逻辑如下[RunInstaller(true)] public partial class ProjectInstaller : System.Configuration.Install.Installer { public ProjectInstaller() { ServiceProcessInstaller processInstaller new ServiceProcessInstaller(); // LocalSystem 权限高多数内网服务器够用 // 需要域账户或者指定用户时改成 manual processInstaller.Account ServiceAccount.LocalSystem; ServiceInstaller serviceInstaller new ServiceInstaller(); serviceInstaller.ServiceName DemoWorkerService; serviceInstaller.DisplayName Demo Worker Service; serviceInstaller.Description 定时执行 job 的后台服务; serviceInstaller.StartType ServiceStartMode.Automatic; Installers.Add(processInstaller); Installers.Add(serviceInstaller); } }Account LocalSystem是个偷懒但常见的选项。它对文件系统近乎完全权限很多坑都能避开但它也是安全评审盯着的地方。如果服务只需访问某个共享目录或数据库更稳妥的做法是建一个最小权限的本地用户Account ServiceAccount.Manual然后加Credentials属性。对于这个 demo按 LocalSystem 跑起来先把链路走通。3.2 sc.exe 快速注册适合拿去验证如果不想带 Installer 工程或者你只是临时部署到测试机sc.exe是手工路径里最好用的工具。我一般会这样做sc.exe create DemoWorkerService binPath D:\services\WorkerServiceDemo.exe start auto DisplayName Demo Worker Service注意两个细节。binPath后必须有一个空格这是 sc.exe 的语法少一个空格就提示参数错误。路径有空格时整串加引号。路径里不要用相对路径服务启动时的工作目录不一定是程序集目录相对路径十次有九次会出问题。这个 demo 给到的 .NET5 发布产物我习惯放到一个不带空格的盘符目录里比如D:\svc\免得路径转义折腾。sc 除了 create常用的还有sc.exe start DemoWorkerService sc.exe query DemoWorkerService sc.exe stop DemoWorkerService sc.exe delete DemoWorkerServicequery的STATE字段停在RUNNING才算真的起来了。很多时候create成功但start失败后面避坑章里我会专门讲。3.3 卸载与升级先停、再删、后覆盖Windows 服务有个很烦的特性exe 正被服务进程占用时你想覆盖 DLL 会提示文件正在被另一进程使用。我拆这个 demo 时专门验证过升级流程正确姿势是这样sc.exe stop DemoWorkerService sc.exe delete DemoWorkerService :: 确认 STATE 变成 STOPPED 之后再做覆盖 xcopy /Y /E publish\* D:\svc\ sc.exe create DemoWorkerService binPath D:\svc\WorkerServiceDemo.exe start auto sc.exe start DemoWorkerService停服务和删服务之间稍微等一下SCM 释放句柄有延迟。有人直接sc.exe stop后马上覆盖文件照样报文件占用就是这个原因。如果删服务时提示 1072 或者指定的服务已标记为删除那说明句柄没释放完等几秒或者重启一下服务管理器。升级时比较稳的做法是发布目录固定成D:\svc新版本文件先放大版本号再覆盖不要直接改文件名。注意installutil 卸载用installutil /u WorkerServiceDemo.exe。如果中途用过sc.exe delete又没有成功删干净再执行 installutil 会提示服务已存在需要先在服务管理器里确认那个名字真的消失了。4. 调式、日志与控制台双模看见服务内部到底发生什么4.1 控制台模式跑能定位 80% 的逻辑问题Windows 服务调试最烦的是看不到输出。但这个 demo 有个非常实用的特性——直接双击 exe 或者用dotnet run跑它会进入控制台模式。这意味着程序集加载问题、配置文件找不到、业务代码异常全部能直接在控制台看到。我排错的第一步永远是先控制台跑一遍逻辑跑通再注册成服务。cd D:\svc WorkerServiceDemo.exe如果是 .NET5 框架依赖发布可以用dotnet WorkerServiceDemo.dll控制台模式下看到Application started. Press CtrlC to shut down.就说明宿主起来了。接下来看有没有业务日志刷出来确认定时任务真的在跑。如果控制台都报错就别折腾服务管理器了问题在代码或者环境。4.2 附加进程调试断点必须在服务启动后控制台模式能看日志能确认整体逻辑但断点调试还是得靠 Visual Studio 附加进程。Debug - Attach to Process - 勾选 Show processes from all users - 找到 WorkerServiceDemo.exe - Attach注意服务要是以 LocalSystem 跑的普通权限看不到这个进程必须勾选 all users且 Visual Studio 要用管理员身份打开。断点位置有讲究Program.cs里的host.Run()之前不要打断点因为 SCM 启动服务时代码还没跑到那里可能就已经被判定启动超时。正确做法是在ExecuteAsync入口打第一个断点。另外一个实用技巧是服务启动时附加往往赶不上可以在Main里加一个Thread.Sleep(5000)的延迟demo 里部分版本加的是if (!Environment.UserInteractive) Thread.Sleep(3000)给调试器争取附加时间。部署时记得把这个延迟注释掉或设成 0。4.3 日志EventLog 和文件日志的选择demo 里默认用了ILogger在控制台模式写控制台注册成服务后控制台输出不可见所以文件日志是第一个要补的设施。这段代码值得抄builder.Logging.AddFile(logs/demo-{Date}.log, options { options.FormatLogFileName f $demo-{DateTime.Now:yyyyMMdd}.log; options.FileSizeLimitBytes 50 * 1024 * 1024; });这是第三方包Serilog.Extensions.Logging.File的写法。如果你不想引第三方包也可以自己写一个ILoggerProvider但没必要Serilog 这套成熟。日志级别默认在appsettings.json里配{ Logging: { LogLevel: { Default: Information, Microsoft: Warning, Microsoft.Hosting.Lifetime: Information } } }Microsoft.Hosting.Lifetime级别的日志会输出服务的启停信息排查服务有没有起来特别有用。文件日志最大的价值是事后分析——服务半夜崩了/var/log 里看时间线EventLog 的好处是 Windows 的事件查看器里能统一看但信息量太少格式也不友好。我的习惯是文件日志记录业务细节EventLog 只写服务的启动、停止、异常重启三个生命周期事件。提示文件日志路径建议配绝对路径比如D:\logs\demo-{Date}.log。以 LocalSystem 跑时相对路径在系统目录下不好找。5. 避坑手册服务启动失败、路径错位、权限不足5.1 错误 1053服务没有及时响应启动或控制请求现象sc.exe start或服务管理器里启动先是正在启动过一会儿弹窗报错误 1053服务没有及时响应启动或控制请求。原因SCM 默认给服务 30 秒启动时限。你的Main里如果做了耗时的初始化比如读大文件、连数据库、调远程接口没到host.Run()就已经超时。另一个高频原因是程序集没找到——binPath写了 exe但同一个目录下的 DLL 发布不完整启动时抛出FileNotFoundExceptionSCM 直接判定没起来。解决先控制台跑一遍排除程序集缺失再看Main里有没有阻塞初始化。常见做法是把耗时操作移进ExecuteAsync的开头让服务先向 SCM 报告已启动再慢慢干活。如果要在启动时就加载用IHostApplicationLifetime.ApplicationStarted回调那个时机 SCM 已确认启动。5.2 附件路径错误工作目录不在程序集目录现象服务在日志里记的路径全是C:\Windows\System32\...按相对路径找appsettings.json找不到整个配置加载失败。原因SCM 启动服务时进程的工作目录CurrentDirectory是System32不是 exe 所在目录。控制台模式没问题服务模式立刻翻车。这是 Windows 服务最典型的路径坑。解决代码里所有路径都基于AppContext.BaseDirectorystring baseDir AppContext.BaseDirectory; string configFilePath Path.Combine(baseDir, appsettings.json);文件日志、读取配置、写附件全部拼接这个目录。日志文件则直接配绝对路径别让它依赖工作目录。5.3 服务显示 RUNNING 但任务没执行DI 生命周期搞错现象服务启动成功状态 RUNNING但等了好几个轮询周期任务一次没跑。日志里什么都没有或者只有启动一条。原因AddSingletonJobService注册成单例没问题怕的是你把DemoWorker也注册成单例或者业务服务注册成Scoped导致注入到 Worker 里时作用域错位。在BackgroundService里用 Scoped 服务会抛Cannot resolve scoped service from root provider。更隐蔽的情况是 Worker 注册没问题但ExecuteAsync一开始就return了比如while条件误写成了while(true)然后被 finally 吞掉异常线程静默退出。解决Worker 必须AddHostedServiceT注册Worker 内部的临时服务用IServiceScopeFactory手动建作用域。这是标准解法using (var scope _scopeFactory.CreateScope()) { var job scope.ServiceProvider.GetRequiredServiceJobService(); await job.ProcessOnceAsync(stoppingToken); }判断任务真的在跑在ExecuteAsync里Task.Delay之前加一行日志确认每轮都进来了。别靠肉眼看要靠日志时间戳。5.4 更新覆盖 DLL 报 file in use停服务后句柄未释放现象sc.exe stop完成后去覆盖D:\svc\WorkerServiceDemo.dllWindows 提示文件被占用替换不了。原因SCM 通知服务停止是异步的服务收到CancellationToken后如果ExecuteAsync里有长任务没退出进程一直活着。sc.exe stop返回时可能只表示已发出停止请求不是真的停了。解决sc.exe query DemoWorkerService确认STATE为STOPPED再覆盖。如果执行停止后长期停在STOP_PENDING检查ExecuteAsync的 finally 里有没有没加超时的等待Task.Delay没有传stoppingToken几乎必然卡死。这个 demo 的写法里Task.Delay(delay, stoppingToken)是能正常退出的关键。真到了杀不掉的地步taskkill /f /pid PID是最后手段但那是后悔药不要靠它维持运维流程。5.5 服务没问题但访问共享路径 401LocalSystem 不总是万能现象服务跑在 A 机器任务去读 B 机器的共享目录日志报Access is denied或者访问 SQL Server 时报Login failed for user。原因LocalSystem 访问网络共享是以机器账户身份过去的不是当前登录用户。跨域或者对端权限严格时机器账户没有被授权。解决用ServiceAccount.Manual指定一个域账户或本机账号并给这个账号配共享目录的最小权限processInstaller.Account ServiceAccount.Manual; // 在服务的 Log On 选项卡里填入 user/pwd或者在 Installer 类里设置demo 是用 LocalSystem 起步的生产环境我建议尽早切换到指定账户否则后面每次换密码都要动服务运维成本很高。6. 让服务更可靠自启动、崩溃重启、心跳确认6.1 恢复选项别让服务死了就永远死了服务挂掉之后能不能自动拉起来由 SCM 的恢复策略控制。这个参数用命令行就可以设置sc.exe failure DemoWorkerService reset 86400 actions restart/5000/restart/10000/restart/60000意思是第一次失败 5 秒后重启第二次 10 秒后第三次以及以后 60 秒后。reset 86400表示一天内失败次数清零。这个参数配合start auto保障服务在机器重启后能拉起进程崩了之后能自动恢复。如果是周期任务场景服务挂了没被拉起数据就漏了。6.2 心跳与自检确认它真的在干活服务跑起来五分钟你知道它还在干活吗靠日志回看有点滞后我的习惯是给服务加一个心跳文件private void WriteHeartbeat() { string heartbeatPath Path.Combine(_heartbeatDir, heartbeat.txt); File.WriteAllText(heartbeatPath, $alive at {DateTimeOffset.Now:O}); }每小时写一次。监控脚本只要检查这个文件的修改时间距离当前时间超过 15 分钟就告警。Windows Server 上不用额外装监控 agent一个计划任务就够了。心跳文件作用很实际排查服务是 RUNNING 但是没干活的隐蔽问题时它是第一手证据——如果心跳时间还在更新进程活着问题的方向就转到业务代码或外部依赖如果心跳停了进程状态就存疑了。我从一处项目之后养成一个习惯每次用sc.exe注册完服务强制走一遍完整流程——sc query看状态、控制台跑一遍、看日志文件有没有滚动、心跳文件是否更新四步全过才敢合上服务器管理器。Windows 服务它不是一件玄学的东西多数翻车都集中在路径、权限、宿主生命周期这三件事上。把这个 demo 的项目文件、Program.cs、Worker 写法吃透你就能在任意一台 Windows Server 上把常驻任务以正式服务跑起来。希望这篇拆解帮到你落地过程顺利。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →