Spiga

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启动流程

  1. 创建WebApplicationBuilder:做一些信息读取
  2. 配置WebApplicationBuilder:各种初始化配置
  3. IOC注册:各种IOC注册,含MVC
  4. Build(): IOC容器初始化——这时,前面配置才生效
  5. Use中间件:管道模型配置
  6. Run():启动监听—Kestrel

三、 WebApplicationBuilder

  • 这是个GOF23种设计模式里面的---创建型设计模式---建造者模式
  • 看源码:里面也就是做了点初始化东西,包括rootpath、environment、configuration等信息---还包括一些自定义配置等,都封装进去。
  • 属性配置Builder
    • Configuration:读取配置文件的帮助类——可以配置新增多个数据源
    • Logging:配置项目的日志组件——常用于替换Log4net
    • Service是IOC注册
  • Host配置Builder
    • 容器替换需要基于Host来替换
    • Kestrel配置——builder.WebHost.ConfigureKestrel
    • 也可以配置基于属性的配置,但不推荐
  • 都是对Builder进行各种配置,各种初始化

四、IOC

1. 上帝视角

  1. 整个启动过程,全程都是IOC&DI!
  2. IOC注册----Build()容器初始化----IOC注册----DI获取
  3. 三类注册:各种默认注册----开发者类注册----MVC注册
  4. 学习过程: 常规注册和注入使用---理解IOC&DI---默认注册源码解读---IOC容器替换和源码解读

2. 内置IOC使用

  1. 建立抽象+实现,启动时注册
  2. 基于容器获取实例
  3. 容器实例注入和使用
  4. 生命周期验证——实例是容器生成的,所有容器也能管理生命周期(瞬时、单例、作用域)
  5. 多种注册方式
  6. 对象集合的获取——多次注册,获取集合
  7. 重复注册和替换——replace的方式才能替换
  8. 核心在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基本过程

  1. 注册Register-------获取实例Resolve
  2. 依赖和注入,构造函数注入-属性注入-方法注入
  3. 生命周期管理,各种注册方式,集合注入等,其实都可以编码实现依赖注入:构造A对象时,需要依赖B对象,那么就先构造B对象传入,也就是能在构造对象时,对象的依赖初始化并注入进去,这种技术手段就叫依赖注入。

IOC和DI是密不可分,要实现IOC,就必须有DI

7. IOC的意义

  1. 面向抽象编程,架构更稳定: TestServiceA---TestServiceA2
  2. 方便扩展,新的实现+注册覆盖: ITestServiceE-ITestServiceEV2--- ---无限的扩展点:可以轻松完成各种扩展
  3. 注入屏蔽细节------使用者需要知道底层细节
  4. 生命周期管理等----不需要再写单例了,直接用容器
  5. 方便AOP扩展

8. 关于IOC的开发建议

  1. 保持面向抽象编程,有接口有实现,优先构造函数注入
  2. 自上而下每一层皆为IOC,贯穿全部层
  3. 关于静态帮助方法:建议改成IOC式,静态方法一般是写扩展方法---除非是既不需要传入参数配置,也不需要扩展升级,纯粹的工具
  4. 组件注册统一封装, AddXXXX
  5. Options+委托模式数据获取

9. IOC三层扩展

  1. 通过IOC注册替换,完成框架的扩展
  • 这是ASP.NET Core最常用的框架扩展点,几乎无处不在
  • IControllerFactory--DefaultControllerFactory--CustomControllerFact 步骤
    • 实现接口
    • IOC注册时替换
  1. 默认容器不支持属性注入、AOP等,可以扩展去支持

    • IControllerActivator--DefaultControllerActivator --CustomControllerActivator 步骤

      • 实现接口
      • IOC注册
      • AddControllersAsServices()
    • 理解:

      • 这也是标准的IOC注册替换的扩展,添加自定义动作
      • 这个也扩展了下默认IOC容器的功能---甚至搞个AOP扩展,也不难的----这个只是小道,满足一些特殊需求
  2. 替换IOC容器,使用Autofac

    1. Nuget添加Autofac.Extensions.DependencyInjection

    2. builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

    3. autofac式注入---N中注册方式--Module

    4. Autofac---AOP扩展---面向切面编程

10. 上帝视角--IOC生命周期理解

前提:IOC容器的2件事儿被拆开设计:注册ServiceCollection 生成ServiceProvider----ServiceCollection.BuildServiceProvider()

生命周期:

  1. 初始化Builder, ConfigBuilder时,只要有ServiceCollection就行;没有的话就保存到委托;
  2. Build()实例化ServiceCollection,各种默认注册—都是基于默认容器
  3. Build()基于Factory支持容器实例替换,注册关系会转换过去
  4. 请求响应时,按HttpContext分配IOC容器Scope实例

11. 源码阅读

  1. 各种IOC注册,源码查看各种注册,系统的各种IOC注册:

    1. 都是写入ServiceCollection
    2. 没有ServiceCollection就先写入委托,后面执行委托即可
    3. 包括开发者注册

    都保存在委托里面

  2. IOC实例化,在Build()时,发生了非常多动作---只是ServiceColletion

    核心在WebHostBuilder. BuildCommonServices

    1. 关于HostStartup
    2. new ServiceCollection(); 实例化ServiceCollection
    3. 添加各种内置注册,注意默认的IOC提供者:services.AddTransient<IServiceProviderFactory,DefaultServiceProviderFactory>();
    4. _configureServices?.Invoke(_context, services);//前面注册的委托执行,其实都是在build里面,而且是在默认注册之后

11. IOC容易替换

Build()里面的GetProviderFromFactory(hostingServices);则是IOC容器替换的核心环节! --到ServiceProvider

  1. 先获取容器实例,这里一定是默认容器
  2. 通过默认容器,再获取容器实例的工厂! ---扩展点就是基于这里实现的
  3. 如果已替换容器工厂,则用工厂创建个新的Provider
  4. 以后程序里面用的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

  1. 找出各种dll
  2. 检测特性
  3. 反射实例,执行Configure

程序启动过程中, IOC容器实例化前,通过反射额外执行了一波逻辑——这就是个扩展点

2. 无侵入式扩展

  1. 项目启动时额外处理逻辑---IWebHostBuilder几乎啥都能干
  2. 扩展CustomHostingStartup,项目内—自动的被执行---优先于默认流程---逼格高一点,但是通常不用这样装---如果想减少修改可以做---如计数统计
  3. 如果能放在项目外----其实是有bug的---可以轻松的修改流程,破坏之前的流程---所以默认是不允许的----希望允许呢?设置一个environmentVariables-- "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES":"Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;XXXX.AgileFramework.WebCore"
  4. 支持无侵入式扩展----SkyAPM----链路追踪,可以把请求的各种参数/结果/时间都收集起来做分析---每个服务都需要的,直接通过HostingStartup来扩展的,需要配置环境变量

六、Option模式

使用IOC时,对象都可以依赖注入,但数据呢?要传值呢?

比如DbContext里面需要获取连接字符串,该如何实现?

  1. DbContext自己指定连接字符串---那就不是集中配置了,有变动得修改底层
  2. 注入IConfiguration, 然后DbContext自己指定路径去获取? --- 也跟1一样
  3. 上端注入个string字符串?一是麻烦,二是如果有后期修改呢?

标准用法: Options模式,跟IOC密切相关的。

1. Options使用过程

  1. 封装实体对象,用来保存数据,无参数构造函数
    1. 向IOC注册数据(程序/配置文件)
    2. 容器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的使用,其实大部分东西都能猜测了!

  1. 启动流程中, AddOptions完成各种相关注册
  2. 基于委托完成数据注册,获取数据时执行委托
  3. IOptions、IOptionsMonitor、IOptionsSnapshot数据的生命周期不一样,有缓存
  4. 源码也挺简单的---也没必要去扩展Options了

5. Options源码解读

Options源码在OptionsServiceCollectionExtensions,源自runtime类库—不能直接调试---提供了最新的下载 https://github.com/dotnet/runtime路径: runtime-main\src\libraries\ externals.csproj----还有很多环节的源码,都在这里看的

  1. IOC注册环节: Build方法的BuilderCommon—就是5个注册
  2. 实例生成,各种注入理解—不同的生命周期
  3. 下一步是执行数据注册---然后就是注入实例

6. IOptions源码解析

IOptions源码其实最简单,步骤详解如下: ---读一次,一直用

  1. 先注册到UnnamedOptionsManager---注入生成就是UnnamedOptionsManager, ----注入一个IOptionsFactory的实例
  2. Value在Get时,第一次是通过factory获取,并保存---后续就复用这个值
  3. UnnamedOptionsManager是单例生命周期---所以只有一个实例,所以这个Value就是缓存------这里是泛型的,其实就是泛型缓存Factory

Factory

  1. 生成时,会注入IConfigureOptions集合,IPostConfigureOptions集合
  2. Create方法,其实就是创建空Options实例,然后遍历执行IConfigureOptions,再遍历执行IPostConfigureOptions,完成数据的生成

Configure方法就是去IOC注册, IConfigureOptions注册给new ConfigureNamedOptions(name, configureOptions)-----多次注册---然后注入给Factory

7. Options使用建议

  • 简单粗暴不更新,就IOptions----其实用的最多
  • 需要更新IOptionsMonitor-------实时支持更新
  • IOptionsSnapshot(除非单次请求内要求保证不变,新的请求用新的数据)---请求处理过程中,可以去Configure配置,新的请求就能用上最新的

总结下: Options使用细节多,源码还算简单,建议大家好好看看

七、Logger组件

日志是项目基础组件了,框架直接内置,不过也基本上都会扩展的

  1. Log控制器,基本使用, ILogger和ILoggerFactory
  2. Log日志过滤简单使用
  3. log4net组件扩展(log4net+kafka+ELK)

1. 上帝视角

Logger就是写日志---一个是日志级别;另外一个是存储介质;再一个是过滤;

  1. 启动时注册, AddLogging---里面会有LogFactory, ILogger
  2. 支持多种日志有序输出,靠的是Provider,会有多个Provider
  3. 支持Filter日志过滤

要扩展日志,一般就是扩展那个存储介质,所以就不换Factory了,增加个Provider,实现自己的日志介质即可

2. 源码研究

还是从Build()方法的BuildCommon的AddLogging开始!

  1. AddLogging注册了Logger和LoggerFactory,都是单例
  2. 生成:其实是通过LoggerFactory,支持注入Provider,也可以直接AddProvider
  3. Factory去CreateLogger,一是缓存,二是把provider集合都传过去了
  4. Logger里面遍历Provider提供的Logger,然后一一执行日志输出
  5. 写日志是LoggerExtensions,执行的就是Logger.log,不同级别就是个参数
  6. 关于Filter,在Add时是配置Option,然后再Factroy注入—RefreshFilters变成通用,保存在logger缓存,传递给Logger,使用时就过滤一下

要扩展日志,就是实现个Provider---Logger---注册进去就行

3. 自定义Logger

写一个自定义日志介质的方式,日志信息我自己写个处理方式

  1. LoggerProvider对接Factory
  2. ConsoleLogger实现接口ILogger
  3. 将LoggerProvider提供给Factory/Builder

这里就是典型的Factory--Provider--Worker

4. 标准组件封装

  1. 封装的组件,不可能是一成不变,需要支持上端的配置---Options式数据传递初始化---CustomConsoleLoggerOptions
  2. 封装Add式组件注册---CustomConsoleLoggerExtensions----方便注册,隐藏注册细节
  3. 委托式初始化

以上就是标准的组件封装,几个元素都是需要的

八、Configuration组件

1. 基本操作

  1. 基本的路径读取---appsettings.json
  2. Bind和Get---对象获取
  3. 命令行参数支持---框架默认添加,三种格式, ---以命令行为准
  4. 多配置文件支持---json-xml-ini
  5. 内存数据提供---memory模式
  6. 所有的配置文件源都是有效的----但是同一个key,后面的覆盖前面的-----命令行是最后注册

2. 上帝视觉

Configuration其实就是包装了一下对文件读取----也支持了下其他模式---

  1. 程序启动时,初始化ConfigurationBuilder,支持添加多种配置文件的Provider
  2. ConfigurationBuilder在Build时,完成数据的初始化,放入内存
  3. 获取的时候,Provider倒序遍历获取,第一个有效—所以后注册的有效

配置文件也是增加个Provider,完成扩展

3. 源码研究

  1. 初始化环节, Build—BuildCommon---new ConfigurationBuilder,Inovke执行的一系列扩展配置
  2. ConfigurationBuilder.Build时---遍历全部的IConfigurationSource,其实就是AddCommand,AddJson,AddXML;---执行Build获得Provider,全部Provider传给ConfigurationRoot----构造时遍历provider,执行Load,完成数据加载,放入到内存里
  3. AddCommandLine源码--- CommandLineConfigurationExtensions给ConfigurationBuilder添加CommandLineConfigurationSource----Build时执行Build得到CommandLineConfigurationProvider---触发Load加载全部数据,保存给父类的Data
  4. 数据如何读取? --就是从ConfigurationRoot用索引获取--- GetConfiguration倒序遍历provider去匹配key,第一个就返回------- SetConfiguration就是遍历provider,都设置这个值

其实直接对着ConfigurationRoot就可以set

4. 扩展

回顾下Configuration核心流程---Builder-支持添加Source(provider)-包了一层Provider(Worker)

  1. CustomConfigurationProvider----提供数据增删改查
  2. CustomConfigurationSource------生成CustomConfigurationProvider
  3. CustomConfigurationExtensions--方便注册
  4. CustomConfigurationOptions-----额外增加的方便配置

九、ChangeToken

Options和Configuration都关联到一个ChangeTokenConfiguration是如何支持的reloadOnChange?其实这只是组件的一个具体知识点

1. 上帝视角

AddJsonFile—reloadOnChange,如果文件更新,程序的数据就更新—其实在Build时数据都已经加载到内存---肯定是文件更新,触发了事件,完成了内存数据的刷新靠的就是ChangeToken.OnChange,到底是发生了什么事儿?

  1. ChangeToken是啥?其实是个生产者消费者的封装,把A注册给B, B发生了,就触发A-------- ChangeToken源码
  2. 配置文件的自动刷新,需要的就是文件监听+刷新数据,关联注册起来---Configuration源码
  3. 文件是如何监听的?源自于System.IO的封装-----FileProvider源码

这其实是3个方向,需分别解读

2. 源码解读

先理解ChangeToken,直接看源码

  1. ChangeToken.Onchange的2个参数分别是生产者Func 消费者Action
  2. ChangeTokenRegistration---构造函数,先得到Token,注册回调---回调里面再注册Token和回调(递归注册,就能持续监听和触发)
  3. 如果要看是哪个ChangeToken---这次看的就是配置文件监听时触发的

总结下: ChangeToken.Onchange是一个 生产者—消费者 关联关系的封装,支持持续生产+消费,不是一次性----就像消息队列(不准确),业务无关 所以不仅是可以为配置文件服务,为各种东西都可以服务,将来路由Route里面还有这个

4. PhysicalFileProvider研究

配置文件用的就是PhysicalFileProvider----然后看PhysicalFilesWatcher---这里是基于System.IO下的基础类库的事件来完成的

具体这个是怎么实现,感兴趣自己看看

结果:通过基础类库完成对文件的监听----任何动作,都转成IChangeToken---经过ChangeToken.Onchange绑定到Configuration的更新动作,然后一切就都ok了

其实IChangeToken--ChangeToken.Onchange都是业务无关,是抽象出来的

5. 配置文件更新源码

有了前面的基础,这会儿再看看配置文件是如何支持reloadOnChange的

  1. 从AddJson开始,返回个委托,包含一些配置, reloadOnChange-provider
  2. 然后就是JsonConfigurationSource,里面的Build就2个:
    • EnsureDefaults—指定FilProvider为PhysicalFileProvider
    • new一个JsonConfigurationProvider()—核心在父类FileConfigurationProvider构造函数里
  3. FileConfigurationProvider构造函数里面,通过ChangeToken.OnChange来注册,生产者是文件监听---消费者是Load加载配置文件数据方法