乐于分享
好东西不私藏

.NET底层源码架构套路:Logger与Configuration双引擎拆解

.NET底层源码架构套路:Logger与Configuration双引擎拆解

一、为什么要把Logger和Configuration放在一起讲?

打开 Microsoft.Extensions.Logging 和 Microsoft.Extensions.Configuration 两个命名空间的源码,你会发现一个惊人的事实:
【核心结论】这两套框架的架构骨架几乎是同一套模具铸造出来的。
维度
Logger 日志系统
Configuration 配置系统
编排者
ILoggerFactory
IConfigurationBuilder
来源
ILoggerProvider
IConfigurationSource
执行者
ILogger (Worker)
IConfigurationProvider (Worker)
门面
LoggerFactory / Logger
ConfigurationRoot
链路
Factory → Provider → Worker
Builder → Source → Provide
  • Logger
     走的是 Factory → Provider → Worker 套路
  • Configuration
     走的是 Builder → Source → Provider 套路

两者核心设计思想完全同源——编排者收集来源 → 来源生产执行者 → 执行者真正干活 → 门面聚合统一对外

理解了其中一套,另一套就是换皮。

但它们的执行细节差异很大

【核心结论】Logger 的多 Provider 是"并行广播"——每条日志分发给所有 Provider;Configuration 的多 Provider 是"优先级覆盖"——后注册的覆盖先注册的。

这个差异是本文的核心对比点。

🔹二、ILogger 日志组件:源码全链路

2.1 上帝视角:日志系统的六个关键动作

对应 PPT 专题5的 6 个要点,日志系统从注册到输出完整链路如下:

① AddLogging() → DI 注册 LoggerFactory(单例) + ILogger<T>(单例)
② AddConsole() → 向 ILoggingBuilder 添加 ILoggerProvider
③ CreateLogger() → 缓存 + 组装聚合 Logger
④ Log() → 聚合 Logger 遍历所有子 Logger 输出
⑤ LogInformation() → 扩展方法,本质调用 Logger.Log()
⑥ Filter → Options 配置 → RefreshFilters() → 级别过滤

2.2 AddLogging():DI 容器注册了什么?

入口在 LoggingServiceCollectionExtensions.AddLogging(),主机 BuildCommon() 默认调用。

逐行解读:

注册项
生命周期
作用
ILoggerFactory → LoggerFactory
Singleton
日志工厂,创建和管理 Logger 实例
ILogger<T> → Logger<T>
Singleton
泛型日志器,T 作为 CategoryName
ILogger → Logger
Singleton
非泛型日志器
LoggerFilterOptions
通过 Options
日志过滤配置

💡知识点:ILogger<T> 是单例,同一个 T 在整个应用生命周期内拿到的是同一个 Logger 实例。Logger 内部只持有 Provider 引用数组,本身无状态,单例安全。

LoggingBuilder 的实现极其简单——它只是 IServiceCollection 的包装器:
它不做任何构建动作,只是一个"收集器"——所有 AddConsole()AddDebug() 都是往 Services 里注册 ILoggerProvider

2.3 LoggerFactory.CreateLogger():缓存 + 组装 Provider

这是日志系统最核心的方法。LoggerFactory 是单例,每次 CreateLogger(categoryName) 都走缓存逻辑。

三个关键设计点:

1. 双检锁缓存lock(_sync) + Dictionary.TryGetValue,保证线程安全且高性能。同一个 categoryName(通常是类全名)永远返回同一个 Logger 实例。

2. 异常隔离ShouldCatchProviderExceptions 控制是否捕获 Provider 创建异常。默认 true——一个 Provider 挂了,不影响其他 Provider。

3. Filter 应用ApplyFilters() 根据 LoggerFilterOptions.Rules 为每个 Provider 选择匹配的过滤规则。

2.4 聚合 Logger:遍历所有子 Logger 输出

业务代码注入的 ILogger<T>,底层拿到的是一个聚合 Logger。它本身不写日志,只做分发。

【核心结论】聚合 Logger 的 Log() 是遍历所有 Provider 的子 Logger 逐个调用的。注册 Console + Debug + File 三个 Provider,一条日志同时输出到三个地方。这与 Configuration 的"后注册覆盖先注册"完全不同。

2.5 LoggerExtensions:LogInformation 到底调了什么?

日常写的 logger.LogInformation("xxx")不是 ILogger 接口的方法,而是 LoggerExtensions 静态类的扩展方法。

六个级别的方法全部是语法糖,最终调用的都是 ILogger.Log(),日志级别只是一个枚举参数。

【核心结论】ILogger 接口只有一个 Log() 方法,扩展性全靠参数。六个日志级别方法全是语法糖。

2.6 Filter 日志过滤:Options + RefreshFilters 全链路

日志过滤是面试高频考点。完整链路:

appsettings.json 配置 → Options 绑定 LoggerFilterOptions → LoggerFactory 构造时 RefreshFilters() → ApplyFilters 为每个 Provider 选择规则 → 打日志时 IsEnabled() 判断级别

LoggerFilterOptions 源码:

配置示例(appsettings.json):

这段配置会被解析为多条 LoggerFilterRule:

ProviderName
CategoryName
LogLevel
(null=全局)
(null=Default)
Information
(null=全局)
System
Warning
(null=全局)
Microsoft
Warning
Console
(null=Default)
Warning

LoggerFactory 构造函数中监听 Options 变更:

ApplyFilters 中选择最匹配的规则:
💡面试考点:规则选择优先级——ProviderName 最长匹配 > CategoryName 最长匹配 > MinLevel > Default。这和路由匹配的"最具体优先"逻辑一致。

2.7 自定义 Logger Provider:四件套标准封装

扩展日志系统,标准做法是实现四个文件:

① Options 配置载体
② Worker —— 实现 ILogger
③ Provider —— 实现 ILoggerProvider
④ Extensions —— 扩展方法,方便注册
注册使用:
⚠踩坑提醒:[ProviderAlias("CustomConsole")] 特性不能忘!没有它,appsettings.json 中的 Filter 规则无法按 Provider 名匹配。

三、IConfiguration 配置组件:源码全链路

3.1 上帝视角:配置系统的五个关键动作

① Build() → BuildCommon 内部 new ConfigurationBuilder()
② AddXxx() → 向 Builder 添加 IConfigurationSource(只登记参数)
③ Build() → 遍历 Source 调用 Build() 得到 Provider
④ 构造 Root → 遍历 Provider 执行 Load(),注册 ChangeToken
⑤ 读取/写入 → 读取倒序遍历,写入全量赋值

3.2 ConfigurationBuilder.Build():编排者触发构建

【核心结论】Sources 列表中的每个 IConfigurationSource 只是一个"任务说明书"——保存文件路径、可选参数等,不做任何 IO。Build() 做两件事:① 让 Source 生产 Provider;② 把 Provider 交给 ConfigurationRoot。

3.3 IConfigurationSource:极简工厂接口

就一个方法。Source.Build() 做的就是 new 一个 Provider,把自己(参数)传进去。
Source 是 Provider 的工厂,Provider 持有 Source 的参数引用。

3.4 IConfigurationProvider:真正干活的执行者

抽象基类 ConfigurationProvider 的核心——一个大小写不敏感的 Dictionary:

3.5 ConfigurationRoot:门面 + 倒序遍历(灵魂代码)

【核心结论】这段代码是整个 Configuration 系统的灵魂——读取倒序遍历(后注册优先),写入正序遍历(全部设置)。这就是为什么 AddCommandLine() 总是最后注册:命令行参数优先级最高,覆盖 appsettings.json。

3.6 JSON 扁平化解析

【核心结论】JSON 扁平化是核心:{ "ConnectionStrings": { "Read": ["a", "b"] } } 被解析为 ConnectionStrings:Read:0=aConnectionStrings:Read:1=b。这就是 config["ConnectionStrings:Read:0"] 能读取数组元素的原因。

3.7 自定义 Configuration Provider:四件套标准封装

① Options 配置载体
② Source —— 描述源(只存参数)
③ Provider —— 执行器(真正干活)
④ Extensions —— 扩展方法
面试考点:Apollo、Nacos、Consul 的 .NET 客户端,底层都是这套模式——实现自己的 IConfigurationSource + IConfigurationProvider,通过 ChangeToken 支持配置热更新。理解了四件套,看任何配置中心客户端源码都豁然开朗。

四、架构对比:两套同源设计的执行差异

4.1 最大的执行差异:并行广播 vs 优先级覆盖

Logger:并行广播

注册 Console + File + Elastic 三个 Provider → 一条日志同时输出到三个地方
Configuration:优先级覆盖

注册 appsettings.json + 环境变量 + 命令行 → 读取时只有最高优先级的生效

【核心结论】为什么不同?因为业务场景不同:日志需要"全量记录"(不能漏掉任何一个介质),配置需要"单一权威"(同一个 key 只有一个最终值)。

4.2 五维对比总表

📌文末总结

两套框架共享同一个设计哲学:编排者只收集不执行 → 来源与执行分离 → 门面聚合统一对外 → 扩展友好开闭原则 → 异常隔离

理解了这套套路,你就能快速读懂 dotnet/runtime 中任何 AddXxx() 扩展方法,自定义日志介质和配置源,在业务开发中复用这套模式。

Logger 是"广播台"——一条消息,所有频道都播;Configuration 是"优先队列"——一个 Key,最后一个说了算。