我们知道当我们创建一个ASP.NET Core程序的时候,在项目的根目录下会自动创建一个Startup.cs
的文件,这个文件里面包含一个名为Startup
的类,这个类是一个很普通的类,只包含了两个看上去很普通的方法ConfigureServices
、Configure
,它没有继承任何的抽象类或者是接口,但是我们却能够通过这两个方法来注册我们Web程序的服务和中间件,如果你是一位初学者,我想你应该和我一样都有这个疑问:ASP.NET Core是如何执行Startup类的?本文将试图从源码角度来理解ASP.NET Core是如何实现这波操作的。
首先来看一下引用Startup
的地方
1 | public static IHostBuilder CreateHostBuilder(string[] args) => |
2 | Host.CreateDefaultBuilder(args) |
3 | .ConfigureWebHostDefaults(webBuilder => |
4 | { |
5 | webBuilder.UseStartup<Startup>(); |
6 | }); |
在Program
类中调用了IHostBuilder
的扩展方法ConfigureWebHostDefaults
,并在该方法的委托参数中调用IWebHostBuilder
的扩展方法UseStartup
注册了Startup
类型,那么我们首先就从UseStartup
方法的源码作为入口,来一探究竟。
# IWebHostBuilder.UseStartup
UseStartup
的泛型版本会调用其非泛型版本,在方法内部会判断webBuilder
是否继承了ISupportsStartup
接口,如果其继承了该接口那么就调用ISupportsStartup
接口的UseStartup
方法。
IWebHostBuilder.UseStartup
的部分源码如下所示:
1 | public static IWebHostBuilder UseStartup<TStartup>(this IWebHostBuilder hostBuilder) where TStartup : class |
2 | { |
3 | return hostBuilder.UseStartup(typeof(TStartup)); |
4 | } |
5 | |
6 | public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType) |
7 | { |
8 | var startupAssemblyName = startupType.GetTypeInfo().Assembly.GetName().Name; |
9 | |
10 | hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName); |
11 | |
12 | // 判断hostBuilder是否继承了ISupportsStartup接口 |
13 | if (hostBuilder is ISupportsStartup supportsStartup) |
14 | { |
15 | return supportsStartup.UseStartup(startupType); |
16 | } |
17 | |
18 | ... |
19 | } |
IWebHostBuilder.UseStartup
源码位置:https://github.com/dotnet/aspnetcore/blob/v3.1.0/src/Hosting/Hosting/src/WebHostBuilderExtensions.cs
在ASP.NET Core 3.1中,hostBuilder
实际是GenericWebHostBuilder
类的实例,该类同时继承了IWebHostBuilder
和ISupportsStartup
接口, 所以最终会调用GenericWebHostBuilder
类的UseStartup
方法。
那GenericWebHostBuilder
的实例是在何时创建的呢?我们需要来看一下ConfigureWebHostDefaults
方法
# ConfigureWebHostDefaults
扩展方法ConfigureWebHostDefaults
的实现非常简单,调用扩展方法ConfigureWebHost
,并在委托里面执行我们的webBuilder.UseStartup<Startup>()
,其实现源码如下:
1 | public static IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure) |
2 | { |
3 | return builder.ConfigureWebHost(webHostBuilder => |
4 | { |
5 | WebHost.ConfigureWebDefaults(webHostBuilder); |
6 | // 执行委托,实际就是 webBuilder.UseStartup<Startup>(); |
7 | configure(webHostBuilder); |
8 | }); |
9 | } |
ConfigureWebHostDefaults
源码位置:https://github.com/dotnet/aspnetcore/blob/v3.1.0/src/DefaultBuilder/src/GenericHostBuilderExtensions.cs
我们继续看一下ConfigureWebHost
方法,看一下webHostBuilder
是个什么东西
# ConfigureWebHost
我们先直接来看一下ConfigureWebHost
的源码:
1 | public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure) |
2 | { |
3 | // 创建GenericWebHostBuilder的实例 |
4 | var webhostBuilder = new GenericWebHostBuilder(builder); |
5 | configure(webhostBuilder); |
6 | builder.ConfigureServices((context, services) => services.AddHostedService<GenericWebHostService>()); |
7 | return builder; |
8 | } |
ConfigureWebHost
的源码位置:https://github.com/dotnet/aspnetcore/blob/v3.1.0/src/Hosting/Hosting/src/GenericHostWebHostBuilderExtensions.cs
到这里我们就能清楚的知道实际是创建了GenericWebHostBuilder
的实例,通过UseStartup
方法的源码我们知道了会去调用GenericWebHostBuilder
实例的UseStartup
方法,所以我们接下来就看看GenericWebHostBuilder.UseStartup
的实现。
# GenericWebHostBuilder.UseStartup
在GenericWebHostBuilder
的实例方法UseStartup
内部会通过反射的方式创建Startup
类的实例,并依次执行实例的ConfigureServices
、ConfigureContainer
和Configure
方法,并且在框架内部这三个实例方法分别由ConfigureServicesBuilder
、ConfigureContainerBuilder
和ConfigureBuilder
包装其执行过程。
IWebHostBuilder.UseStartup
方法是可以被调用多次的,但是最终只会执行最后一次调用传递的类型。
我们来看一下GenericWebHostBuilder.UseStartup
的源码,以对核心逻辑加了中文注释:
1 | public IWebHostBuilder UseStartup(Type startupType) |
2 | { |
3 | // 保存启动类的类型 |
4 | _builder.Properties["UseStartup.StartupType"] = startupType; |
5 | _builder.ConfigureServices((context, services) => |
6 | { |
7 | // 判断待执行的类型是否与保存的类型一致 |
8 | // 如果调用了多次IWebHostBuilder.UseStartup,这里将只会执行最后一次添加的类型 |
9 | if (_builder.Properties.TryGetValue("UseStartup.StartupType", out var cachedType) && (Type)cachedType == startupType) |
10 | { |
11 | UseStartup(startupType, context, services); |
12 | } |
13 | }); |
14 | |
15 | return this; |
16 | } |
17 | |
18 | private void UseStartup(Type startupType, HostBuilderContext context, IServiceCollection services) |
19 | { |
20 | var webHostBuilderContext = GetWebHostBuilderContext(context); |
21 | var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)]; |
22 | |
23 | ExceptionDispatchInfo startupError = null; |
24 | object instance = null; |
25 | ConfigureBuilder configureBuilder = null; |
26 | |
27 | try |
28 | { |
29 | if (typeof(IStartup).IsAssignableFrom(startupType)) |
30 | { |
31 | throw new NotSupportedException($"{typeof(IStartup)} isn't supported"); |
32 | } |
33 | if (StartupLoader.HasConfigureServicesIServiceProviderDelegate(startupType, context.HostingEnvironment.EnvironmentName)) |
34 | { |
35 | throw new NotSupportedException($"ConfigureServices returning an {typeof(IServiceProvider)} isn't supported."); |
36 | } |
37 | |
38 | // 创建Startup类的实例 |
39 | instance = ActivatorUtilities.CreateInstance(new HostServiceProvider(webHostBuilderContext), startupType); |
40 | context.Properties[_startupKey] = instance; |
41 | |
42 | // 通过反射找到ConfigureServices方法,并包装成ConfigureServicesBuilder对象 |
43 | var configureServicesBuilder = StartupLoader.FindConfigureServicesDelegate(startupType, context.HostingEnvironment.EnvironmentName); |
44 | var configureServices = configureServicesBuilder.Build(instance); |
45 | // 执行ConfigureServices方法 |
46 | configureServices(services); |
47 | |
48 | // 通过反射找到ConfigureContainer方法,并包装成ConfigureContainerBuilder对象 |
49 | var configureContainerBuilder = StartupLoader.FindConfigureContainerDelegate(startupType, context.HostingEnvironment.EnvironmentName); |
50 | if (configureContainerBuilder.MethodInfo != null) |
51 | { |
52 | var containerType = configureContainerBuilder.GetContainerType(); |
53 | _builder.Properties[typeof(ConfigureContainerBuilder)] = configureContainerBuilder; |
54 | |
55 | var actionType = typeof(Action<,>).MakeGenericType(typeof(HostBuilderContext), containerType); |
56 | |
57 | // 找到当前实例的私有ConfigureContainer方法,并创建该方法的委托 |
58 | var configureCallback = GetType().GetMethod(nameof(ConfigureContainer), BindingFlags.NonPublic | BindingFlags.Instance) |
59 | .MakeGenericMethod(containerType) |
60 | .CreateDelegate(actionType, this); |
61 | |
62 | // 执行ConfigureContainer方法的委托 |
63 | typeof(IHostBuilder).GetMethods().First(m => m.Name == nameof(IHostBuilder.ConfigureContainer)) |
64 | .MakeGenericMethod(containerType) |
65 | .InvokeWithoutWrappingExceptions(_builder, new object[] { configureCallback }); |
66 | } |
67 | |
68 | // 通过反射找到Configure方法,并包装成ConfigureBuilder对象 |
69 | configureBuilder = StartupLoader.FindConfigureDelegate(startupType, context.HostingEnvironment.EnvironmentName); |
70 | } |
71 | catch (Exception ex) when (webHostOptions.CaptureStartupErrors) |
72 | { |
73 | startupError = ExceptionDispatchInfo.Capture(ex); |
74 | } |
75 | |
76 | services.Configure<GenericWebHostServiceOptions>(options => |
77 | { |
78 | options.ConfigureApplication = app => |
79 | { |
80 | startupError?.Throw(); |
81 | |
82 | if (instance != null && configureBuilder != null) |
83 | { |
84 | // 执行Configure方法 |
85 | configureBuilder.Build(instance)(app); |
86 | } |
87 | }; |
88 | }); |
89 | } |
90 | |
91 | private void ConfigureContainer<TContainer>(HostBuilderContext context, TContainer container) |
92 | { |
93 | var instance = context.Properties[_startupKey]; |
94 | var builder = (ConfigureContainerBuilder)context.Properties[typeof(ConfigureContainerBuilder)]; |
95 | // 执行Startup实例的ConfigureContainer方法 |
96 | builder.Build(instance)(container); |
97 | } |
GenericWebHostBuilder.UseStartup
源码位置:https://github.com/dotnet/aspnetcore/blob/v3.1.0/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs
StartupLoader.FindConfigureServicesDelegate
方法内部会通过反射的方式寻找ConfigureServices
方法,并创建返回ConfigureServicesBuilder
的实例。ConfigureServicesBuilder
内部规定了ConfigureServices
方法只能有一个参数,并且该参数类型必须为IServiceCollection
。
StartupLoader.FindConfigureContainerDelegate
方法内部会通过反射的方式寻找ConfigureContainer
方法,并创建返回ConfigureContainerBuilder
的实例,ConfigureContainer
方法并不是必须的,只有当我们在Startup
中实现了ConfigureContainer
方法才会去执行。ConfigureContainerBuilder
内部执行ConfigureContainer
方法时只会传递当前IOC容器的实例对象,这也就规定了ConfigureContainer
方法只能有一个参数。
StartupLoader.FindConfigureDelegate
方法内部会通过反射的方式寻找Configure
方法,并创建返回ConfigureBuilder
实例。ConfigureBuilder
内部允许Configure
方法可以有多个参数,执行时会从IOC容器内获取这些参数的实例。
在ConfigureWebHostDefaults
方法中调用了扩展方法ConfigureWebHost
,并在委托中执行了我们传过来的委托,也就是代码webBuilder.UseStartup<Startup>()
。
StartupLoader
源码位置:https://github.com/dotnet/aspnetcore/blob/v3.1.0/src/Hosting/Hosting/src/Internal/StartupLoader.csConfigureServicesBuilder
源码位置:https://github.com/dotnet/aspnetcore/blob/v3.1.0/src/Hosting/Hosting/src/Internal/ConfigureServicesBuilder.csConfigureContainerBuilder
源码位置:https://github.com/dotnet/aspnetcore/blob/v3.1.0/src/Hosting/Hosting/src/Internal/ConfigureContainerBuilder.csConfigureBuilder
源码位置:https://github.com/dotnet/aspnetcore/blob/v3.1.0/src/Hosting/Hosting/src/Internal/ConfigureBuilder.cs
# 最后
通过分析源码我们知道了框架执行Startup
类的详细过程,其通过反射的方式依次执行ConfigureServices
、ConfigureContainer
和Configure
方法,并对它们的参数都做了一定的约束。到此我们就弄明白了文章开头时的那个疑问。