.NET Worker Service(Windows服务)实现OnCustomCommand()的问题排查
.NET Worker Windows服务自定义命令未生效问题排查与最佳实践
问题背景
基于.NET Worker开发Windows服务,服务继承BackgroundService类,通过ExecuteAsync()维护生命周期。需要通过OnCustomCommand()响应服务控制器的自定义命令,但BackgroundService无此方法。参考实现了继承WindowsServiceLifetime的CustomService类,但该类完全未生效(未记录启动日志),同时存在以下疑问:
- 是否可在不安装服务的情况下调试?
Program.cs配置是否正确?- 代码中存在哪些不符合最佳实践的地方?
相关代码
Program.cs
class Program { static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => { if (WindowsServiceHelpers.IsWindowsService()) services.AddSingleton<IHostLifetime, CustomService>(); services.AddHostedService<WindowsBackgroundService>(); }) .ConfigureLogging((hostContext, logging) => { logging.AddConfiguration(hostContext.Configuration.GetSection("Logging")); logging.AddEventLog(); }) .UseWindowsService(); }
WindowsBackgroundService类
public sealed class WindowsBackgroundService : BackgroundService { private readonly ILogger<WindowsBackgroundService> _logger; private readonly IConfiguration _configuration; public WindowsBackgroundService(ILogger<WindowsBackgroundService> logger, IConfiguration configuration) { _logger = logger; _configuration = configuration; } public override async Task StartAsync(CancellationToken stoppingToken) { try { apiSendTimer = new System.Timers.Timer(); apiSendTimer.Elapsed += OnApiSendTimerElapsed; retrieveSpanTimer = new System.Timers.Timer(); retrieveSpanTimer.Elapsed += OnRetrieveSpanTimerElapsed; SetRetrieveSpanTimer(); while (!stoppingToken.IsCancellationRequested) { } } catch (OperationCanceledException ex) { EnsureDirectoryExists(ErrorlogFilePath); string logErrorMessage = ($"Error {DateTime.Now} ExecuteAsyn() cancelled : {ex.Message} "); File.AppendAllText(ErrorlogFilePath, logErrorMessage + Environment.NewLine); } catch (Exception ex) { _logger.LogError(ex, "{Message}", ex.Message); EnsureDirectoryExists(ErrorlogFilePath); string logErrorMessage = ($"Error {DateTime.Now} ExecuteAsyn() cancelled : {ex.Message} "); File.AppendAllText(ErrorlogFilePath, logErrorMessage + Environment.NewLine); Environment.Exit(1); } }
CustomService.cs
namespace App.WindowsService { internal class CustomService : WindowsServiceLifetime { private readonly WindowsBackgroundService _backgroundService; private readonly ILogger<CustomService> _logger; public event EventHandler CustomCommandEvent; private int cutsomCommand = 129; private string ErrorlogFilePath = "C:\\WindowsServicesTest\\WorkerService\\BgLogFileErrors.txt"; public CustomService(WindowsBackgroundService backgroundService, IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor, ILogger<CustomService> logger) : base(environment, applicationLifetime, loggerFactory, optionsAccessor) { _backgroundService = backgroundService ?? throw new ArgumentNullException(nameof(backgroundService)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } protected override void OnStart(String[] args) { base.OnStart(args); try { _backgroundService.EnsureDirectoryExists(ErrorlogFilePath); string logMessage = $"Serivce Started at {DateTime.Now}"; File.AppendAllText(ErrorlogFilePath, logMessage + Environment.NewLine); } catch (Exception ex) { _backgroundService.EnsureDirectoryExists(ErrorlogFilePath); string logErrorMessage = ($"WIndows Service: {DateTime.Now} Error in OnStart method: {ex.Message}"); File.AppendAllText(ErrorlogFilePath, logErrorMessage + Environment.NewLine); } } protected override void OnStop() { base.OnStop(); try { _backgroundService.EnsureDirectoryExists(ErrorlogFilePath); string logMessage = $"Serivce Stopped at {DateTime.Now}"; File.AppendAllText(ErrorlogFilePath, logMessage + Environment.NewLine); } catch (Exception ex) { _backgroundService.EnsureDirectoryExists(ErrorlogFilePath); string logErrorMessage = ($"WIndows Service: {DateTime.Now} Error in OnStart method: {ex.Message}"); File.AppendAllText(ErrorlogFilePath, logErrorMessage + Environment.NewLine); } } protected override void OnCustomCommand(Int32 command) { base.OnCustomCommand(command); try { if (command == this.cutsomCommand) { _backgroundService.EnsureDirectoryExists(ErrorlogFilePath); string logErrorMessage = ($"Received custom command {DateTime.Now} "); File.AppendAllText(ErrorlogFilePath, logErrorMessage + Environment.NewLine); _backgroundService.OnCustomCommandReceived(); } } catch (Exception ex) { _backgroundService.EnsureDirectoryExists(ErrorlogFilePath); string logErrorMessage = ($"Error while getting/parsing timespan {DateTime.Now} - {ex.Message} "); File.AppendAllText(ErrorlogFilePath, logErrorMessage + Environment.NewLine); } } } }
问题排查与修复
1. CustomService未生效的核心原因
UseWindowsService()方法会自动注册WindowsServiceLifetime为单例,你的代码中ConfigureServices的注册逻辑在UseWindowsService()之后执行,导致自定义的CustomService被覆盖。同时,CustomService构造函数直接依赖WindowsBackgroundService(瞬态生命周期),与自身单例生命周期不匹配,会引发静默异常导致初始化失败。
修正后的Program.cs配置
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseWindowsService() .ConfigureServices((hostContext, services) => { // 直接注册CustomService,覆盖默认的WindowsServiceLifetime services.AddSingleton<IHostLifetime, CustomService>(); services.AddHostedService<WindowsBackgroundService>(); }) .ConfigureLogging((hostContext, logging) => { logging.AddConfiguration(hostContext.Configuration.GetSection("Logging")); logging.AddEventLog(); });
修正CustomService构造函数
移除对WindowsBackgroundService的直接依赖,改用服务提供者获取实例:
public CustomService(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor, ILogger<CustomService> logger, IServiceProvider serviceProvider) : base(environment, applicationLifetime, loggerFactory, optionsAccessor) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); }
2. 无需安装即可调试Windows服务
两种调试方式:
- 控制台模式运行:直接启动项目,服务以控制台应用运行,可直接设置断点调试。如需模拟服务模式,可添加命令行参数(如
--run-as-service),在代码中判断并启用对应逻辑。 - Visual Studio调试:在项目属性的「调试」选项卡中,设置启动参数为
--console,选择「控制台应用」启动即可直接调试。
3. 代码最佳实践修正
(1)修复StartAsync阻塞问题
StartAsync中的空while循环会阻塞线程,应调用基类方法,将持续运行逻辑移至ExecuteAsync:
public override async Task StartAsync(CancellationToken stoppingToken) { try { apiSendTimer = new System.Timers.Timer(); apiSendTimer.Elapsed += OnApiSendTimerElapsed; retrieveSpanTimer = new System.Timers.Timer(); retrieveSpanTimer.Elapsed += OnRetrieveSpanTimerElapsed; SetRetrieveSpanTimer(); await base.StartAsync(stoppingToken); } // 异常处理... } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // 空实现或添加持续运行逻辑 await Task.Delay(Timeout.Infinite, stoppingToken); }
(2)统一日志记录
放弃手动写入文件,使用依赖注入的ILogger,已配置的AddEventLog和文件日志(通过appsettings.json配置)可统一管理日志:
// 替换文件写入为日志记录 _logger.LogInformation("Service Started at {Time}", DateTime.Now);
(3)避免硬编码路径
将日志路径配置到appsettings.json,通过IConfiguration读取:
{ "ServiceSettings": { "ErrorLogPath": "C:\\WindowsServicesTest\\WorkerService\\BgLogFileErrors.txt" } }
读取方式:
var errorLogPath = _configuration["ServiceSettings:ErrorLogPath"];
内容的提问来源于stack exchange,提问作者adarsh augustine
相关产品推荐
相关产品推荐

