.NET 控制台后台程序实践细节总结

使用 Host 通用主机和直接创建手动创建 ServiceCollection 构建 DI 容器的区别

主要区别对比

特性 ServiceCollection Host
功能完整性 基础 DI 容器 完整应用框架(DI + 配置 + 日志 + 生命周期)
配置系统 需手动添加 默认集成(自动加载 appsettings.json)
日志系统 需手动配置 默认集成(Console/Debug/EventLog 等)
后台服务 不支持 支持 IHostedService/BackgroundService
生命周期管理 手动管理作用域 自动管理应用生命周期
环境区分 需手动实现 内置开发/生产环境支持
扩展性 有限 通过 Configure* 方法高度可扩展

ServiceCollection 示例

C# 复制代码
// 手动管理所有组件
var services = new ServiceCollection();

// 注入日志操作
services.AddLogging(builder => builder.AddNLog("NLog.dev.config"));

// 注入配置
ConfigurationBuilder builder = new();
builder.AddAppJsonFile("appsettings.json", optional: false, reloadOnChange: true);
builder.AddCommandLine(args);
IConfigurationRoot root = builder.Build();

// 绑定配置
services.AddOptions().Configure<AppSettings>(root.Bind);
services.AddOptions().Configure<DatabaseConfig>(root.GetSection("database").Bind);

// 注册服务
services.AddSingleton<IMyService, MyService>();
services.AddTransient<Worker>();

/* 其他服务注入 */

// 手动构建容器
using var provider = services.BuildServiceProvider();

// 手动创建作用域
using var scope = provider.CreateScope();
var worker = scope.ServiceProvider.GetRequiredService<Worker>();
worker.Execute();

Host 示例

C# 复制代码
var host = Host.CreateDefaultBuilder(args)
  //.ConfigureAppConfiguration((context, config) =>{ /* 默认加载 appsetting.json 和对应的环境配置 */})
  //.ConfigureLogging((context, logging) => {/* 使用 NLog 无需再设置 Logging */})
  .ConfigureServices((context, services) =>
  {
      // 绑定配置信息
      services.Configure<AppSettings>(context.Configuration);
      services.Configure<DatabaseConfig>(context.Configuration.GetRequiredSection("database")); 
      // 注册后台服务
      services.AddHostedService<WorkerService>();
      services.AddScoped<MyTaskService>();      
      /* 其他服务注入 */
  })
  // 使用 NLog 日志并替换自带的日志,需要安装 NLog.Extensions.Hosting
  .UseNLog(new NLogProviderOptions() { ReplaceLoggerFactory = true })
  .UseConsoleLifetime()
  .Build();

// 注册关闭事件
//var appLifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();
//appLifetime.ApplicationStopping.Register(() => commandManager.Dispose());

// 运行主机
await host.RunAsync();

NLog 实践细节

日志作用域的传递性

  • logger 会自动继承上下文,调用位置的参数会传递到内部调用的对象

  • 自动处理异步方法的传递,需要使用 await ,日志作用域通过 AsyncLocal<T> 会自动在异步调用链中流动

  • Parallel.For、new Thread()、Task.Run 无法自动传递作用域

  • 使用作用域可以优化日志文件的管理和层次结构

  • 示例

    C# 复制代码
    public class HandlerTaskService(PluginTaskService pluginTaskService, ILogger<HandlerTaskService> logger)
    {
        private readonly CancellationTokenSource cts = new();
        public async Task Start(HandlerConfig handler, CancellationToken token)
        {
            // 向内部的 Plugin 传递日志作用域参数 HandlerName
            using var scope = logger.BeginScope("Handler:{HandlerName}", handler.Name);
            logger.LogInformation("正在启动 [{name}] 处理程序...", handler.Name);
            var tasks = handler.Plugins.Select(plugin => pluginTaskService.Start(plugin, handler, cts.Token));
            await Task.WhenAll(tasks); // 必须 await
        }
    }
    
    public class PluginTaskService(IPluginServiceFactory factory, ILogger<PluginTaskService> logger)
    {
        public async Task Start(PluginConfig plugin, HandlerConfig handler, CancellationToken _token)
        {
            // 此时 logger 包含调用该方法时的日志作用域上下文,同时向内部传递 PluginName 参数
            using var scope = logger.BeginScope("Plugin:{PluginName}", plugin.Name);
            logger.LogInformation("启动插件 [{name}] ...");
            await Task.Delay(1000, _token);
        }
    }

配置文件

xml 复制代码
<!-- NLog.config -->
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <!-- 获取 logger 设置的 category , ${logger:shortName=true} 不含命名空间的类名称 -->
    <variable name="category" value="${logger}" />
    <!-- 获取 scope 设置的 HandlerName 参数, :whenEmpty=Shared 提供了空参数时的默认值-->
    <variable name="handler" value="${scopeproperty:item=HandlerName:whenEmpty=Shared}" />
    <!-- 使用 HandlerName 参数作为目录结构的组成部分 -->
    <variable name="dir" value="${basedir}/logs/${handler}" />

    <variable name="layout" value="${longdate} ${uppercase:${level}:padding=-5}: [${category}] ${message}" />
    <variable name="consoleLayout" value="${longdate} ${uppercase:${level}:padding=-5}: [${handler}][${category}] ${message}" />

    <targets async="true">
            <target name="console" xsi:type="ColoredConsole" layout="${consoleLayout}"/>
            <target name="file" xsi:type="File" layout="${layout}"
                            fileName="${dir}/${shortdate}.log"
                            concurrentWrites="true"
                            keepFileOpen="true"
                            archiveFileName="${dir}/archives/${shortdate}_{#}.log"
                            archiveAboveSize="10485760"
                            maxArchiveFiles="10"
                            archiveNumbering="Rolling"
                            maxArchiveDays="30" />
    </targets>
</nlog>
xml 复制代码
<!-- NLog.dev.config -->
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
	<include file="NLog.config" />
	<rules>
		<logger name="*" minlevel="Debug" writeTo="console,file" />
		<!--<logger name="*" minlevel="Error" writeTo="errfile"/>-->
		<!--<logger name="*" minlevel="Warning" writeTo="warnfile" final="true" />-->
	</rules>
</nlog>
相关推荐
小蒜学长20 分钟前
编程语言在线学习平台的设计与实现(代码+数据库+LW)
java·spring boot·后端·编程语言在线学习平台·在线编码
zhoujungis2 小时前
登录态 1 小时就掉?Django + Vue 双 Token 认证实战 | 尘世栈帧
后端
椒颜皮皮虾྅2 小时前
让文档不止于文字:DeploySharp + PP-Structure,打通版面分析与表格重建
开发语言·学习·c#·.net·cuda
玩泥巴的2 小时前
可能是最现代化的微信生态 .NET 服务端 SDK
ai·微信·.net·aot
武子康2 小时前
同一台 ESP32,GSM 能消回声,VoIP 为什么不行?
人工智能·后端·agent
用户69371750013842 小时前
AI 能写代码,但工程架构还是应该由人来定
前端·后端
lqbzlll2 小时前
Spring Boot 开启虚拟线程后接口反而变慢了?4 个排查方向和对应修法
java·spring boot·后端
回家路上绕了弯2 小时前
让 AI 成为团队的共同能力
后端
70后写java的老码农2 小时前
一个 starter 通吃 9 家主流大模型:Spring Boot 3.x + Spring AI 1.x 极简落地
后端
武子康3 小时前
MultiNet 只听到一次命令,为什么连续触发?
人工智能·后端·agent