You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 13:32:03