ASP.NET Core 7 源码阅读1:启动过程
2023-05-08 18:38:14一、从.NET Framework到.NET Core
1. 本质
.NET Framework:描述的是上层应用框架,底层就只支持windows平台, BCL CLR都是只有一个
.NET Core:
- 2016年.NETCore1.0,特点是3个CLR共存的(NET Framework、NER Core、XAMARIN)
- 最后个版本是.NET Core3.1
.NET5:终结3个分支,统一,一个CLR,一个BCL——2020年全球疫情,导致很多内容没完成
.NET6:口号也是统一平台,各种该实现的都实现了一下
.NET7:进一步完善统一,没有太大的变化,缝缝补补
2. 跨平台的理解
运行时CLR——CoreCLR
C#程序——》编译器——》DLL/EXE(metadata、IL)——》CLR/JIT——》机器码
不能跨平台是因为只有一个CLR,不同的CLR匹配不同的平台
以前不能开发Linux的CLR,是因为NET Framework底层库依赖window,如IIS,画图库等
3. ASP.NET Core有哪些好处的功能
-
依赖注入
-
日志系统架构
-
引入了一个跨平台的网络服务器,kestrel
-
试用appsettings来配置工程
-
试用startup来注册服务
-
更好的支持异步编程
-
对于跨网站请求的预防和保护机制
-
验证令牌 @Html.AntiForgrtyToken
-
使用HTTPS
-
设置CSP(内容安全策略):定义哪些资源可以被加载和执行,减少XSS攻击风险
context.Response.Headers.Add("Content-Security-Policy", "default-src 'self'; script-src 'self' https://xxx.com");
-
强大的验证和数据绑定功能:如[BindRequired]、[BindNever]属性
-
-
使用安全API端点设计:如JWT进行认证
4. ASP.NET Core有哪些更好的地方
- 跨平台
- 对架构本身安装没有依赖,因为所有的依赖都跟程序本身在一起
- 处理请求的效率更高,能够处理更多请求
- 更多安装配置方法
二、ASP.NET Core启动流程
- 创建WebApplicationBuilder:做一些信息读取
- 配置WebApplicationBuilder:各种初始化配置
- IOC注册:各种IOC注册,含MVC
- Build(): IOC容器初始化——这时,前面配置才生效
- Use中间件:管道模型配置
- Run():启动监听—Kestrel
三、 WebApplicationBuilder
- 这是个GOF23种设计模式里面的---创建型设计模式---建造者模式
- 看源码:里面也就是做了点初始化东西,包括rootpath、environment、configuration等信息---还包括一些自定义配置等,都封装进去。
- 属性配置Builder
- Configuration:读取配置文件的帮助类——可以配置新增多个数据源
- Logging:配置项目的日志组件——常用于替换Log4net
- Service是IOC注册
- Host配置Builder
- 容器替换需要基于Host来替换
- Kestrel配置——builder.WebHost.ConfigureKestrel
- 也可以配置基于属性的配置,但不推荐
- 都是对Builder进行各种配置,各种初始化
四、IOC
1. 上帝视角
- 整个启动过程,全程都是IOC&DI!
- IOC注册----Build()容器初始化----IOC注册----DI获取
- 三类注册:各种默认注册----开发者类注册----MVC注册
- 学习过程: 常规注册和注入使用---理解IOC&DI---默认注册源码解读---IOC容器替换和源码解读
2. 内置IOC使用
- 建立抽象+实现,启动时注册
- 基于容器获取实例
- 容器实例注入和使用
- 生命周期验证——实例是容器生成的,所有容器也能管理生命周期(瞬时、单例、作用域)
- 多种注册方式
- 对象集合的获取——多次注册,获取集合
- 重复注册和替换——replace的方式才能替换
- 核心在WebHostBuilder.BuilderCommonServices
4. IOC和DI
IOC可不仅仅是开发者,而是贯穿整个框架的,需要认真如何理解IOC & DI
- Inversion of Control:控制反转,简称IOC
- Dependency Injection:依赖注入,简称DI
- Dependence Inversion Principle:依赖倒置原则:面向对象语言程序设计时,高层模块不要依赖于低层模块,二者应该通过抽象来依赖,简单来说就是依赖抽象,而不是依赖细节! DIP是IOC的核心理论基础
5. 理解IOC & DI
- 依赖细节,会导致低层的变化影响高层。
- 依赖抽象,低层的变化就不会影响高层,保持稳定,可扩展
- 控制反转IoC是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。
- 依赖倒置,倒置的什么? --高层不再直接依赖低层,而是依赖于抽象
- 控制反转,反转的什么? --高层以前是直接依赖于低层,现在依赖于抽象,把控制权交给第三方
6. IOC基本过程
- 注册Register-------获取实例Resolve
- 依赖和注入,构造函数注入-属性注入-方法注入
- 生命周期管理,各种注册方式,集合注入等,其实都可以编码实现依赖注入:构造A对象时,需要依赖B对象,那么就先构造B对象传入,也就是能在构造对象时,对象的依赖初始化并注入进去,这种技术手段就叫依赖注入。
IOC和DI是密不可分,要实现IOC,就必须有DI
7. IOC的意义
- 面向抽象编程,架构更稳定: TestServiceA---TestServiceA2
- 方便扩展,新的实现+注册覆盖: ITestServiceE-ITestServiceEV2--- ---无限的扩展点:可以轻松完成各种扩展
- 注入屏蔽细节------使用者需要知道底层细节
- 生命周期管理等----不需要再写单例了,直接用容器
- 方便AOP扩展
8. 关于IOC的开发建议
- 保持面向抽象编程,有接口有实现,优先构造函数注入
- 自上而下每一层皆为IOC,贯穿全部层
- 关于静态帮助方法:建议改成IOC式,静态方法一般是写扩展方法---除非是既不需要传入参数配置,也不需要扩展升级,纯粹的工具
- 组件注册统一封装, AddXXXX
- Options+委托模式数据获取
9. IOC三层扩展
- 通过IOC注册替换,完成框架的扩展
- 这是ASP.NET Core最常用的框架扩展点,几乎无处不在
- IControllerFactory--DefaultControllerFactory--CustomControllerFact 步骤
- 实现接口
- IOC注册时替换
-
默认容器不支持属性注入、AOP等,可以扩展去支持
-
IControllerActivator--DefaultControllerActivator --CustomControllerActivator 步骤
- 实现接口
- IOC注册
- AddControllersAsServices()
-
理解:
- 这也是标准的IOC注册替换的扩展,添加自定义动作
- 这个也扩展了下默认IOC容器的功能---甚至搞个AOP扩展,也不难的----这个只是小道,满足一些特殊需求
-
-
替换IOC容器,使用Autofac
-
Nuget添加Autofac.Extensions.DependencyInjection
-
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
-
autofac式注入---N中注册方式--Module
-
Autofac---AOP扩展---面向切面编程
-
10. 上帝视角--IOC生命周期理解
前提:IOC容器的2件事儿被拆开设计:注册ServiceCollection 生成ServiceProvider----ServiceCollection.BuildServiceProvider()
生命周期:
- 初始化Builder, ConfigBuilder时,只要有ServiceCollection就行;没有的话就保存到委托;
- Build()实例化ServiceCollection,各种默认注册—都是基于默认容器
- Build()基于Factory支持容器实例替换,注册关系会转换过去
- 请求响应时,按HttpContext分配IOC容器Scope实例
11. 源码阅读
-
各种IOC注册,源码查看各种注册,系统的各种IOC注册:
- 都是写入ServiceCollection
- 没有ServiceCollection就先写入委托,后面执行委托即可
- 包括开发者注册
都保存在委托里面
-
IOC实例化,在Build()时,发生了非常多动作---只是ServiceColletion
核心在WebHostBuilder. BuildCommonServices
- 关于HostStartup
- new ServiceCollection(); 实例化ServiceCollection
- 添加各种内置注册,注意默认的IOC提供者:services.AddTransient<IServiceProviderFactory
,DefaultServiceProviderFactory>(); - _configureServices?.Invoke(_context, services);//前面注册的委托执行,其实都是在build里面,而且是在默认注册之后
11. IOC容易替换
Build()里面的GetProviderFromFactory(hostingServices);则是IOC容器替换的核心环节! --到ServiceProvider
- 先获取容器实例,这里一定是默认容器
- 通过默认容器,再获取容器实例的工厂! ---扩展点就是基于这里实现的
- 如果已替换容器工厂,则用工厂创建个新的Provider
- 以后程序里面用的Provider就是这个了---所以替换了factory,就能替换掉容器实例
static IServiceProvider GetProviderFromFactory(IServiceCollection collection)
{
var provider = collection.BuildServiceProvider();
var factory = provider.GetService<IServiceProviderFactory<IServiceProvider>>();
if(factory != null && factory is not DefaultProviderFactory)
{
using(privder) //释放privder,再返回新的
{
return factory.CreateServiceProvider(factory.CreateBuilder(collection));
}
}
return provider;
}
12. 手撸IOC容器和替换
- 准备好新的IOC容器, IXXXContainer
- 准备容器Factory,得实现IServiceProviderFactory
- 准备XXXContainerBuilder:真正负责完成生成和转化动作
- 准备XXXServiceProvider,其实就是最终提供实例获取的地方,里面包裹上内置容器,将来获取实例就是从这里走
- 初始化时指定IOC容器工厂
工厂---找Builder---Builder完成映射关系转换+生成Provider----Provider是包了一层的Container,即Factory+Builder+Provider
五、HostingStartup
1. 在Build()时,有个HostStartup
- 找出各种dll
- 检测特性
- 反射实例,执行Configure
程序启动过程中, IOC容器实例化前,通过反射额外执行了一波逻辑——这就是个扩展点
2. 无侵入式扩展
- 项目启动时额外处理逻辑---IWebHostBuilder几乎啥都能干
- 扩展CustomHostingStartup,项目内—自动的被执行---优先于默认流程---逼格高一点,但是通常不用这样装---如果想减少修改可以做---如计数统计
- 如果能放在项目外----其实是有bug的---可以轻松的修改流程,破坏之前的流程---所以默认是不允许的----希望允许呢?设置一个environmentVariables-- "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES":"Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;XXXX.AgileFramework.WebCore"
- 支持无侵入式扩展----SkyAPM----链路追踪,可以把请求的各种参数/结果/时间都收集起来做分析---每个服务都需要的,直接通过HostingStartup来扩展的,需要配置环境变量
六、Option模式
使用IOC时,对象都可以依赖注入,但数据呢?要传值呢?
比如DbContext里面需要获取连接字符串,该如何实现?
- DbContext自己指定连接字符串---那就不是集中配置了,有变动得修改底层
- 注入IConfiguration, 然后DbContext自己指定路径去获取? --- 也跟1一样
- 上端注入个string字符串?一是麻烦,二是如果有后期修改呢?
标准用法: Options模式,跟IOC密切相关的。
1. Options使用过程
- 封装实体对象,用来保存数据,无参数构造函数
- 向IOC注册数据(程序/配置文件)
- 容器DI获取使用, IOptions<T>
2. Options多种注册方式
- Configure
- ConfigureAll
- PostConfigure
- PostConfigureAll
- AddOptions
- 注册分默认注册是名字为null,以及带名字注册Post就是后面执行的
3. Options三种获取方式
- IOptions
:只能获取默认名字, - IOptionsMonitor
:既可以默认名字,也可以带别名 - 一次读取,重复使用,有变更会自动更新
- CurrentValue就是DefaultName---Get时是通过缓存GetOrAdd—通过factory读取
- 也是Singleton,也是一直缓存? ---在构造函数加了个InvokeChanged,负责在数据变化后,触发动作,清理缓存,把新数据读进去,下次直接用
- IOptionsSnapshot
:既可以默认名字,也可以带别名 - 请求内缓存重用,不同请求分别读取
- Value就是DefaultName---Get时是通过缓存GetOrAdd---通过factory读取
- Scope生命周期----一次请求内,重复读取,是用的缓存;不同次请求,数据是重新存的;
4. 上帝视角
对照前面Options的使用,其实大部分东西都能猜测了!
- 启动流程中, AddOptions完成各种相关注册
- 基于委托完成数据注册,获取数据时执行委托
- IOptions、IOptionsMonitor、IOptionsSnapshot数据的生命周期不一样,有缓存
- 源码也挺简单的---也没必要去扩展Options了
5. Options源码解读
Options源码在OptionsServiceCollectionExtensions,源自runtime类库—不能直接调试---提供了最新的下载 https://github.com/dotnet/runtime路径: runtime-main\src\libraries\ externals.csproj----还有很多环节的源码,都在这里看的
- IOC注册环节: Build方法的BuilderCommon—就是5个注册
- 实例生成,各种注入理解—不同的生命周期
- 下一步是执行数据注册---然后就是注入实例
6. IOptions源码解析
IOptions源码其实最简单,步骤详解如下: ---读一次,一直用
- 先注册到UnnamedOptionsManager---注入生成就是UnnamedOptionsManager, ----注入一个IOptionsFactory的实例
- Value在Get时,第一次是通过factory获取,并保存---后续就复用这个值
- UnnamedOptionsManager是单例生命周期---所以只有一个实例,所以这个Value就是缓存------这里是泛型的,其实就是泛型缓存Factory
Factory
- 生成时,会注入IConfigureOptions
集合,IPostConfigureOptions 集合 - Create方法,其实就是创建空Options实例,然后遍历执行IConfigureOptions,再遍历执行IPostConfigureOptions,完成数据的生成
Configure方法就是去IOC注册, IConfigureOptions
7. Options使用建议
- 简单粗暴不更新,就IOptions----其实用的最多
- 需要更新IOptionsMonitor-------实时支持更新
- IOptionsSnapshot(除非单次请求内要求保证不变,新的请求用新的数据)---请求处理过程中,可以去Configure配置,新的请求就能用上最新的
总结下: Options使用细节多,源码还算简单,建议大家好好看看
七、Logger组件
日志是项目基础组件了,框架直接内置,不过也基本上都会扩展的
- Log控制器,基本使用, ILogger和ILoggerFactory
- Log日志过滤简单使用
- log4net组件扩展(log4net+kafka+ELK)
1. 上帝视角
Logger就是写日志---一个是日志级别;另外一个是存储介质;再一个是过滤;
- 启动时注册, AddLogging---里面会有LogFactory, ILogger
- 支持多种日志有序输出,靠的是Provider,会有多个Provider
- 支持Filter日志过滤
要扩展日志,一般就是扩展那个存储介质,所以就不换Factory了,增加个Provider,实现自己的日志介质即可
2. 源码研究
还是从Build()方法的BuildCommon的AddLogging开始!
- AddLogging注册了Logger和LoggerFactory,都是单例
- 生成:其实是通过LoggerFactory,支持注入Provider,也可以直接AddProvider
- Factory去CreateLogger,一是缓存,二是把provider集合都传过去了
- Logger里面遍历Provider提供的Logger,然后一一执行日志输出
- 写日志是LoggerExtensions,执行的就是Logger.log,不同级别就是个参数
- 关于Filter,在Add时是配置Option,然后再Factroy注入—RefreshFilters变成通用,保存在logger缓存,传递给Logger,使用时就过滤一下
要扩展日志,就是实现个Provider---Logger---注册进去就行
3. 自定义Logger
写一个自定义日志介质的方式,日志信息我自己写个处理方式
- LoggerProvider对接Factory
- ConsoleLogger实现接口ILogger
- 将LoggerProvider提供给Factory/Builder
这里就是典型的Factory--Provider--Worker
4. 标准组件封装
- 封装的组件,不可能是一成不变,需要支持上端的配置---Options式数据传递初始化---CustomConsoleLoggerOptions
- 封装Add式组件注册---CustomConsoleLoggerExtensions----方便注册,隐藏注册细节
- 委托式初始化
以上就是标准的组件封装,几个元素都是需要的
八、Configuration组件
1. 基本操作
- 基本的路径读取---appsettings.json
- Bind和Get---对象获取
- 命令行参数支持---框架默认添加,三种格式, ---以命令行为准
- 多配置文件支持---json-xml-ini
- 内存数据提供---memory模式
- 所有的配置文件源都是有效的----但是同一个key,后面的覆盖前面的-----命令行是最后注册
2. 上帝视觉
Configuration其实就是包装了一下对文件读取----也支持了下其他模式---
- 程序启动时,初始化ConfigurationBuilder,支持添加多种配置文件的Provider
- ConfigurationBuilder在Build时,完成数据的初始化,放入内存
- 获取的时候,Provider倒序遍历获取,第一个有效—所以后注册的有效
配置文件也是增加个Provider,完成扩展
3. 源码研究
- 初始化环节, Build—BuildCommon---new ConfigurationBuilder,Inovke执行的一系列扩展配置
- ConfigurationBuilder.Build时---遍历全部的IConfigurationSource,其实就是AddCommand,AddJson,AddXML;---执行Build获得Provider,全部Provider传给ConfigurationRoot----构造时遍历provider,执行Load,完成数据加载,放入到内存里
- AddCommandLine源码--- CommandLineConfigurationExtensions给ConfigurationBuilder添加CommandLineConfigurationSource----Build时执行Build得到CommandLineConfigurationProvider---触发Load加载全部数据,保存给父类的Data
- 数据如何读取? --就是从ConfigurationRoot用索引获取--- GetConfiguration倒序遍历provider去匹配key,第一个就返回------- SetConfiguration就是遍历provider,都设置这个值
其实直接对着ConfigurationRoot就可以set
4. 扩展
回顾下Configuration核心流程---Builder-支持添加Source(provider)-包了一层Provider(Worker)
- CustomConfigurationProvider----提供数据增删改查
- CustomConfigurationSource------生成CustomConfigurationProvider
- CustomConfigurationExtensions--方便注册
- CustomConfigurationOptions-----额外增加的方便配置
九、ChangeToken
Options和Configuration都关联到一个ChangeTokenConfiguration是如何支持的reloadOnChange?其实这只是组件的一个具体知识点
1. 上帝视角
AddJsonFile—reloadOnChange,如果文件更新,程序的数据就更新—其实在Build时数据都已经加载到内存---肯定是文件更新,触发了事件,完成了内存数据的刷新靠的就是ChangeToken.OnChange,到底是发生了什么事儿?
- ChangeToken是啥?其实是个生产者消费者的封装,把A注册给B, B发生了,就触发A-------- ChangeToken源码
- 配置文件的自动刷新,需要的就是文件监听+刷新数据,关联注册起来---Configuration源码
- 文件是如何监听的?源自于System.IO的封装-----FileProvider源码
这其实是3个方向,需分别解读
2. 源码解读
先理解ChangeToken,直接看源码
- ChangeToken.Onchange的2个参数分别是生产者Func 消费者Action
- ChangeTokenRegistration---构造函数,先得到Token,注册回调---回调里面再注册Token和回调(递归注册,就能持续监听和触发)
- 如果要看是哪个ChangeToken---这次看的就是配置文件监听时触发的
总结下: ChangeToken.Onchange是一个 生产者—消费者 关联关系的封装,支持持续生产+消费,不是一次性----就像消息队列(不准确),业务无关 所以不仅是可以为配置文件服务,为各种东西都可以服务,将来路由Route里面还有这个
4. PhysicalFileProvider研究
配置文件用的就是PhysicalFileProvider----然后看PhysicalFilesWatcher---这里是基于System.IO下的基础类库的事件来完成的
具体这个是怎么实现,感兴趣自己看看
结果:通过基础类库完成对文件的监听----任何动作,都转成IChangeToken---经过ChangeToken.Onchange绑定到Configuration的更新动作,然后一切就都ok了
其实IChangeToken--ChangeToken.Onchange都是业务无关,是抽象出来的
5. 配置文件更新源码
有了前面的基础,这会儿再看看配置文件是如何支持reloadOnChange的
- 从AddJson开始,返回个委托,包含一些配置, reloadOnChange-provider
- 然后就是JsonConfigurationSource,里面的Build就2个:
- EnsureDefaults—指定FilProvider为PhysicalFileProvider
- new一个JsonConfigurationProvider()—核心在父类FileConfigurationProvider构造函数里
- FileConfigurationProvider构造函数里面,通过ChangeToken.OnChange来注册,生产者是文件监听---消费者是Load加载配置文件数据方法