ASP.NET Core是如何执行Startup类的

ASP.NET Core是如何执行Startup类的

我们知道当我们创建一个ASP.NET Core程序的时候,在项目的根目录下会自动创建一个Startup.cs的文件,这个文件里面包含一个名为Startup的类,这个类是一个很普通的类,只包含了两个看上去很普通的方法ConfigureServicesConfigure,它没有继承任何的抽象类或者是接口,但是我们却能够通过这两个方法来注册我们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类的实例,该类同时继承了IWebHostBuilderISupportsStartup接口, 所以最终会调用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类的实例,并依次执行实例的ConfigureServicesConfigureContainerConfigure方法,并且在框架内部这三个实例方法分别由ConfigureServicesBuilderConfigureContainerBuilderConfigureBuilder包装其执行过程。

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.cs
ConfigureServicesBuilder源码位置:https://github.com/dotnet/aspnetcore/blob/v3.1.0/src/Hosting/Hosting/src/Internal/ConfigureServicesBuilder.cs
ConfigureContainerBuilder源码位置:https://github.com/dotnet/aspnetcore/blob/v3.1.0/src/Hosting/Hosting/src/Internal/ConfigureContainerBuilder.cs
ConfigureBuilder源码位置:https://github.com/dotnet/aspnetcore/blob/v3.1.0/src/Hosting/Hosting/src/Internal/ConfigureBuilder.cs

# 最后

通过分析源码我们知道了框架执行Startup类的详细过程,其通过反射的方式依次执行ConfigureServicesConfigureContainerConfigure方法,并对它们的参数都做了一定的约束。到此我们就弄明白了文章开头时的那个疑问。

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×