推荐直接使用 microsoft.extensions.hosting.windowsservices,因其为微软官方推荐方案,兼容 .net 6+ 宿主模型、原生集成 di 与配置,无需额外依赖;topshelf 已归档,不支持现代 .net,易出现服务未真正运行、日志失败及安装报错等问题。

Topshelf 是个已被官方归档的库(2023 年停止维护),现在直接用 Microsoft.Extensions.Hosting.WindowsServices 更稳、更轻、无额外依赖。如果你手头已有 Topshelf 项目,迁移成本不高;但新项目别再引入它了。
为什么 Topshelf 现在不推荐用
Topshelf 本质是封装了 ServiceBase 的调用逻辑,并提供命令行交互能力。但它长期没适配 .NET 6+ 的宿主模型,也不支持现代 DI 和配置系统原生集成。官方 GitHub 已标记为 Archived,NuGet 上最新版(4.2.1)仍基于 .NET Framework 4.6.1,无法真正跑在 .NET 6+ 的 Windows 服务模式下。
常见现象:
- 安装后服务状态显示“已启动”,但
ExecuteAsync根本没执行——其实是 Topshelf 启动了控制台宿主,却没正确切换到 Windows Service 上下文 -
topshelf install报错 “Could not load file or assembly 'System.ServiceProcess'”——.NET Core/5+ 默认不带这个程序集 - 日志写不到事件查看器,
EventLog.WriteEntry抛SecurityException,因为 Topshelf 没帮你提权注册事件源
.NET 6+ 替代方案:用 Worker Service + WindowsServices 包
这是微软当前唯一推荐路径,零学习成本,且天然兼容所有 IHostedService 生态(如 BackgroundService、Quartz.NET、Hangfire.Client)。
操作步骤:
- 新建项目选
Worker Service模板(不是 “Windows Service (.NET Framework)”) - NuGet 安装
Microsoft.Extensions.Hosting.WindowsServices - 修改
Program.cs:在Host.CreateApplicationBuilder后加builder.Services.AddHostedService<worker>()</worker>,再调用builder.UseWindowsService() - 确保发布时选
win-x64或win-x86,不要用AnyCPU(尤其当你引用了 x86 原生 DLL)
关键代码片段:
var builder = Host.CreateApplicationBuilder(args); builder.UseWindowsService(); // ← 这一行必须有,且要在 Build() 前 builder.Services.AddHostedService<worker>(); var host = builder.Build(); host.Run();</worker>
sc.exe 安装服务时最常踩的坑
不用 InstallUtil.exe,也不用 Topshelf 的 install 命令。直接用系统自带的 sc.exe,干净、可控、权限明确。
典型命令:
sc create MyService binPath= "C:\MyService\MyService.exe" start= auto obj= "LocalSystem"
注意细节:
-
binPath=后面**必须有空格**,且路径不能带引号(除非含空格,才用英文双引号包裹整个路径) -
obj=指定运行账户:LocalSystem权限最高,但访问网络资源受限;若需连 SQL 或 SMB 共享,请用DOMAIN\user并提前赋权 - 安装前务必以管理员身份运行 CMD/PowerShell,否则
Access Denied - 卸载用
sc delete MyService,别漏掉已存在的同名服务残留
OnStart 超时失败?你可能根本没在用 Windows Service 模式
很多人以为只要项目类型叫“Windows Service”就自动进服务上下文,其实不是。.NET 6+ 必须显式调用 UseWindowsService(),否则即使你用 sc create 注册了,进程仍是普通控制台应用,只是被 SCM(服务控制管理器)拉起来而已。
验证方式:
- 任务管理器 → 详细信息 → 找到你的服务进程 → 右键“转到服务”,如果没关联,说明没走 Windows Service 流程
- 在
Worker.ExecuteAsync开头加一行_logger.LogInformation("Running as Windows Service: {IsService}", Environment.IsService()),输出false就是没生效 - 服务启动超时(30 秒)不是因为你代码慢,而是 SCM 根本没收到“服务已就绪”信号——因为宿主没注册服务通知回调
真正容易被忽略的一点:发布后的 EXE 文件名,必须和 sc create 里写的 binPath 完全一致,包括大小写。Windows 文件系统不区分大小写,但 SCM 解析 binPath 时会校验签名和路径有效性,名字对不上会导致静默失败。











