星际矿业机库飞机类型切换的办法-星际矿业机库如何切换飞机类型
2026-07-08 3387598
2026-07-08 0
在 review 一个同事的 PR 时,我发现项目里 Program.cs 的日志注册方式,跟我自己常写的不一样。顺手翻了一下团队里另外几个仓库,发现至少存在三种"看起来都能跑"的写法:

写法 A:显式创建 Logger 实例,交给 Host.UseSerilog
var logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();builder.Host.UseSerilog(logger);
写法 B:通过 IServiceCollection 注册
builder.Services.AddSerilog(options =>{ options.ReadFrom.Configuration(configuration);});
写法 C:写静态 Log.Logger,再调用无参 UseSerilog
Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();SerilogHostBuilderExtensions.UseSerilog(builder);
三种写法跑起来都能输出日志,console 里该有的内容一条不少。问题来了:它们到底是不是等价的?团队里要不要统一成一种?
这篇文章记录我把这个问题查清楚的过程。
我最先怀疑的方向是——这三种写法分别对应 Serilog 不同的历史阶段,而不是同一时期的三种平行选择。查了 serilog-aspnetcore 仓库的 Release Note 才确认了这个猜测:
也就是说,写法 A、C 里用到的 Host.UseSerilog(...)(挂在 IHostBuilder 上的扩展方法)目前还活着,但写法 B 的 Services.AddSerilog(...) 是 Serilog 官方在 8.0 之后明确推荐的新主线。这不是"哪种风格更优雅"的争论,而是库作者已经把方向定了。
到这一步我意识到,光看"能不能跑"远远不够,得搞清楚这三种写法背后分别接入了哪一套机制。
要理解这三种写法的差异,得先弄明白一件事:Serilog 本身和 Microsoft.Extensions.Logging(以下简称 MEL)是两套独立的日志系统,Serilog 要"接管"ASP.NET Core 的日志输出,靠的是一个适配器:
你的业务代码│注入 ILogger
无论走 UseSerilog 还是 AddSerilog,本质上都是在做同一件事:往 DI 容器里注册一个 ILoggerFactory(或等价物),这个 factory 内部包一层 SerilogLoggerProvider,把所有通过 ILogger 产生的日志事件,转发给真正的 Serilog ILogger 去落地。
区别在于:这个"真正的 Serilog ILogger"从哪来、谁拥有它、谁负责释放它。这正是三种写法分歧的核心。
Host.UseSerilog(logger)var logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();builder.Host.UseSerilog(logger);
IHostBuilder.UseSerilog 的真实签名大致是:
public static IHostBuilder UseSerilog(this IHostBuilder builder,Serilog.ILogger logger = null,bool dispose = false)
传入一个已经创建好的 logger 实例时,Serilog 不会再帮你管理它的生命周期——dispose 参数默认是 false。这意味着:
logger 局部变量没有被赋给 Log.Logger(静态属性),所以通过 Serilog.Log.Information(...) 这种静态方式是打不出日志的,只有走 ILogger 注入才有效。 应用退出时,没有人调用这个 logger 的 Dispose(),如果 sink 里有 File、网络型 sink(如 Seq)等带缓冲/后台线程的资源,存在日志丢失或文件未刷盘的风险。 builder.Services.AddSerilog(...)(官方现在推荐的主线)builder.Services.AddSerilog(options =>{ options.ReadFrom.Configuration(configuration);});
这是 Serilog.Extensions.Hosting 包提供的扩展方法,直接挂在 IServiceCollection 上,不依赖 IHostBuilder。它的好处有三点:
Log.Logger 静态属性的依赖,整个日志配置走依赖注入,对单元测试更友好(不会出现"测试之间互相污染静态 logger"的问题)。 支持两阶段初始化(Two-Stage Initialization),下一节细讲,这是它相对前两种写法最大的实际优势。 由 DI 容器统一管理生命周期,应用关闭时会随 host 一起正确释放底层资源。 官方仓库现在给出的"标准模板"基本都是这个形态:
Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateBootstrapLogger(); // 注意:这里用的是 CreateBootstrapLogger(),不是 CreateLogger()var builder = WebApplication.CreateBuilder(args);builder.Services.AddSerilog((services, lc) => lc.ReadFrom.Configuration(builder.Configuration).ReadFrom.Services(services)// 关键:可以读取 DI 容器里注册的服务.Enrich.FromLogContext().WriteTo.Console());
Log.Logger 静态属性,再调用无参 UseSerilog()Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();SerilogHostBuilderExtensions.UseSerilog(builder);
这里 UseSerilog() 不传 logger 参数(logger 默认为 null)。当参数为 null 时,Serilog 内部走的逻辑是:自动回退去读取静态的 Serilog.Log.Logger。所以写法 C 实际上等价于:
Log.Logger = ...;// 全局唯一的静态 Loggerbuilder.Host.UseSerilog(); // 隐式使用 Log.Logger
这种写法的特点:
Log.Logger 是静态字段,进程内全局唯一、随时可用——这也是为什么很多人喜欢在 Program.cs 顶部、WebApplicationBuilder 还没创建之前就先配置好 logger:可以用 try/catch 包住整个启动过程,连宿主构建阶段抛出的异常都能被记录下来。 副作用:Log.Logger 是进程级单例,如果项目里有集成测试在同一进程里反复 WebApplicationFactory 启动多个 host,多次重复赋值 Log.Logger 会相互覆盖,这是这种写法被诟病比较多的点(Serilog 仓库的 issue 区有专门讨论:#105)。| 维度 | 写法 A(局部 logger Host.UseSerilog(logger)) | 写法 B(Services.AddSerilog) | 写法 C(Log.Logger 无参 UseSerilog()) |
|---|---|---|---|
是否依赖静态 Log.Logger | 否 | 否 | 是 |
能否用 Log.Information(...) 静态调用 | 不能 | 不能(除非你额外手动设置 Log.Logger) | 能 |
是否支持两阶段初始化(ReadFrom.Services) | 不直接支持 | 支持 | 不直接支持 |
| 进程退出时是否自动释放 sink 资源 | 否(dispose 默认 false,需自己管理) | 是(由 DI 容器接管生命周期) | 需要手动 Log.CloseAndFlush() |
| 官方当前推荐程度 | 历史写法,仍可用 | 8.0 之后的主推写法 | 经典写法,仍广泛使用,但有静态状态的副作用 |
| 适合捕获"宿主构建阶段"异常 | 一般 | 一般(除非配合 Bootstrap Logger) | 强(配置发生在 WebApplicationBuilder 创建之前) |
查到这里,我发现真正应该学的不是"三选一",而是 Serilog 官方在 Serilog.AspNetCore ≥ 6.0 之后大力推广的模式——两阶段初始化,它解决了一个写法 A、B、C 都没单独解决好的根本矛盾:
两阶段初始化的做法是:先用一个极简的 Bootstrap Logger 顶住启动阶段,等 host 构建完、配置和服务都齐备了,再换成正式 Logger。
using Serilog;// === 第一阶段:Bootstrap Logger ===// 此时还没有 builder.Configuration,只能写最基础的 sink(通常是 Console)// 作用仅仅是兜底捕获 Program.cs 顶层、Host 构建过程中的异常Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateBootstrapLogger(); // 注意:用 CreateBootstrapLogger,而非 CreateLoggertry{ Log.Information("应用程序正在启动");var builder = WebApplication.CreateBuilder(args);// === 第二阶段:正式 Logger ===// 通过 AddSerilog 的回调形式,可以拿到 IServiceProvider(services)// 这意味着自定义 Enricher 如果需要注入 IHttpContextAccessor 等服务,// 在这里是可以做到的,写法 A / C 都做不到这一点builder.Services.AddSerilog((services, loggerConfig) => loggerConfig.ReadFrom.Configuration(builder.Configuration) // 读取 appsettings.json 中的 Serilog 配置节.ReadFrom.Services(services)// 关键:可读取 DI 容器里注册的服务/Sink/Enricher.Enrich.FromLogContext() // 支持 LogContext.PushProperty 动态属性.WriteTo.Console());// Bootstrap 阶段的 sink 在这里要重新声明一遍,// 因为正式 Logger 会完全替换掉 Bootstrap Loggervar app = builder.Build();app.UseSerilogRequestLogging(); // 记录每个 HTTP 请求的耗时、状态码等信息app.MapGet("/", () => "Hello World!");app.Run();}catch (Exception ex){ Log.Fatal(ex, "应用程序异常终止");}finally{ Log.CloseAndFlush(); // 确保所有缓冲中的日志事件都被写出,再退出进程}
这个写法把写法 A(早期可用、能捕获启动异常)和写法 B(能读 DI 服务、生命周期托管给容器)的优点结合在了一起,是目前 Serilog 官方文档和 Serilog.AspNetCore NuGet 包说明页给出的标准范式。
回到最初的问题——三种写法要不要统一?我的结论是:
新项目,统一用「两阶段初始化Services.AddSerilog」。这是当前官方主推、生态文档最完整、长期维护性最好的方式。 写法 A(Host.UseSerilog(logger),手动管理实例)不建议在新代码里使用:dispose 默认为 false 这个细节很容易被忽略,遗留的资源释放问题排查成本不低。 写法 C(Log.Logger 无参 UseSerilog())不是错的,很多线上项目仍在用,且对捕获"宿主构建阶段异常"确实友好;但要清楚它引入了进程级静态状态,在写集成测试、或者一个进程里跑多个 host 实例时要格外小心,必要时显式调用 Log.CloseAndFlush() 做清理。 如果项目历史悠久、Host.UseSerilog 还跑在 7.x 及更早版本的 Serilog.AspNetCore 上,升级到 8.0 时要注意:IWebHostBuilder.UseSerilog() 这个重载已被移除,编译都过不了,必须迁移到 IHostBuilder.UseSerilog() 或 IServiceCollection.AddSerilog()。UseSerilogRequestLogging() 要放在哪顺手记一下查资料时发现的一个容易踩的坑——app.UseSerilogRequestLogging() 这个中间件只会记录它之后的中间件管线所消耗的时间。如果把它放在 UseStaticFiles()、UseRouting() 之后,静态文件请求的日志会被排除在外(这有时反而是你想要的,可以用来给高频但没什么信息量的请求降噪);但如果误放在认证、路由中间件之前,记录到的耗时和状态码可能不准确。建议默认紧跟在 app.Build() 之后,按需再微调顺序。