互联网技术 · 2024年2月24日 0

ASP.NET Core Startup初始化问题的深入研究

这篇文章主要介绍了深入探究ASP.NET Core Startup初始化问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

前言

Startup类相信大家都比较熟悉,在我们使用ASP.NET Core开发过程中经常用到的类,我们通常使用它进行IOC服务注册,配置中间件信息等。虽然它不是必须的,但是将这些操作统一在Startup中做处理,会在实际开发中带来许多方便。当我们谈起Startup类的时候你有没有好奇过以下几点

为何我们自定义的Startup可以正常工作。

我们定义的Startup类中ConfigureServices和Configure只能叫这个名字才能被调用到吗?

在使用泛型主机(IHostBuilder)时Startup的构造函数,为何只支持注入IWebHostEnvironment、IHostEnvironment、IConfiguration。

ConfigureServices方法为何只能传递IServiceCollection实例。

Configure方法的参数为何可以是所有在IServiceCollection注册服务实例。

在ASP.NET Core结合Autofac使用的时候为何我们添加的ConfigureContainer方法会被调用。

带着以上几点疑问,我们将在本篇文章中探索Startup的源码,来了解Startup初始化过程到底为我们做了些什么。

Startup的另类指定方式

在日常编码过程中,我们通常使用UseStartup的方式来引入Startup类。但是这并不是唯一的方式,还有一种方式是在配置节点中指定Startup所在的程序集来自动查找Startup类,这个我们可以在GenericWebHostBuilder的构造函数源码中的找到相关代码相信熟悉ASP.Net Core启动流程的同学对GenericWebHostBuilder这个类都比较了解。ConfigureWebHostDefaults方法中其实调用了ConfigureWebHost方法,ConfigureWebHost方法中实例化了GenericWebHostBuilder对象,启动流程不是咱们的重点,所以这里只是简单描述一下。直接找到我们需要的代码如下所示

//判断是否配置了StartupAssembly参数
if (!string.IsNullOrEmpty(webHostOptions.StartupAssembly))
{
try
{
//根据你配置的程序集去查找Startup
var startupType = StartupLoader.FindStartupType(webHostOptions.StartupAssembly, webhostContext.HostingEnvironment.EnvironmentName);
UseStartup(startupType, context, services);
}
catch (Exception ex) when (webHostOptions.CaptureStartupErrors)
{
//此处省略代码省略
}
}

这里我们可以看出来,我们需要配置StartupAssembly对应的程序集,它可以通过StartupLoader的FindStartupType方法加载程序集中对应的类。我们还可以看到它还传递了EnvironmentName环境变量,至于它起到了什么作用,我们继续往下看。

首先我们需要找到webHostOptions.StartupAssembly是如何被初始化的,在WebHostOptions的构造函数中我们找到了StartupAssembly初始化的地方

StartupAssembly = configuration[WebHostDefaults.StartupAssemblyKey];

从这里也可以看出来它的值来于配置,它的key来自WebHostDefaults.StartupAssemblyKey这个常量值,最后我们找到了的值为

public static readonly string StartupAssemblyKey = “startupAssembly”;

也就是说只要我们给startupAssembly配置Startup所在的程序集名称,它就可以在程序集中查找Startup类进行初始化,如下所示

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureHostConfiguration(config=> {
List<KeyValuePair<string, string>> keyValuePairs = new List<KeyValuePair<string, string>>();
//配置Startup所在的程序集名称
keyValuePairs.Add(new KeyValuePair<string, string>(“startupAssembly”, “Startup所在的程序集名称”);
config.AddInMemoryCollection(keyValuePairs);
})
.ConfigureWebHostDefaults(webBuilder =>
{
//这样的话这里就可以省略了
//webBuilder.UseStartup<Startup>();
});

回到上面的思路,我们在StartupLoader类中查看FindStartupType方法,来看下它是通过什么规则来查找Startup的精简之后的代码大致如下

public static Type FindStartupType(string startupAssemblyName, string environmentName)
{
var assembly = Assembly.Load(new AssemblyName(startupAssemblyName));
//名称Startup+环境变量的类比如(StartupDevelopment)
var startupNameWithEnv = “Startup” + environmentName;
//名称为Startup的类
var startupNameWithoutEnv = “Startup”;

// 先查找包含名称Startup+环境变量的相关类,如果找不到则查找名称为Startup的类
var type =
assembly.GetType(startupNameWithEnv) ??
assembly.GetType(startupAssemblyName + “.” + startupNameWithEnv) ??
assembly.GetType(startupNameWithoutEnv) ??
assembly.GetType(startupAssemblyName + “.” + startupNameWithoutEnv);

if (type == null)
{
// 如果上述规则找不到,则在程序集定义的所有类中继续查找
var definedTypes = assembly.DefinedTypes.ToList();

var startupType1 = definedTypes.Where(info => info.Name.Equals(startupNameWithEnv, StringComparison.OrdinalIgnoreCase));
var startupType2 = definedTypes.Where(info => info.Name.Equals(startupNameWithoutEnv, StringComparison.OrdinalIgnoreCase));

var typeInfo = startupType1.Concat(startupType2).FirstOrDefault();
if (typeInfo != null)
{
type = typeInfo.AsType();
}
}
//最终返回Startup类型
return type;
}

通过上述代码我们可以看到在通过配置指定程序集时是如何查找指定规则的Startup类的,基本上可以理解为先去查找名称为Startup+环境变量的类,如果找不到则继续查找名称为Startup的类,最终会返回Startup的类型传递给UseStartup方法。其实我们最常使用的UseStartup()方法最终也是转换成UseStartup(typeof(T))的方式,所以最终这两种方式走到了相同的地方,接下来我们步入正题,来一起探究一下Starup究竟是如何被初始化的。

Startup的构造函数

相信对Startup有所了解的同学们都比较清楚,在使用泛型主机(IHostBuilder)时Startup的构造函数只支持注入IWebHostEnvironment、IHostEnvironment、IConfiguration,这个在微软官方文档中也有介绍,如果还有不熟悉这个操作的请先反思一下自己,然后在查阅微软官方文档。接下来我们就从源码着手,来探究一下它到底是如何做到的。沿着上述的操作,继续查看UseStartup里的代码找到了如下的实现

//创建Startup实例
object instance = ActivatorUtilities.CreateInstance(new HostServiceProvider(webHostBuilderContext), startupType);

这里的startupType就是我们传递的Startup类型,关于ActivatorUtilities这个类还是比较实用的,它为我们提供了许多帮助我们实例化对象的方法,在日常编程中如果有需要可以使用这个类。上面的ActivatorUtilities的CreateInstance方法的功能就是根据传递IServiceProvider类型的对象去实例化指定的类型对象,我们这里的类型就是startupType。它的使用场景就是,如果某个类型需要用过有参构造函数去实例化,而构造函数的参数可以来自于IServiceProvider的实例,那么使用这个方法就在合适不过了。上面的代码传递的IServiceProvider的实例是HostServiceProvider对象,接下来我们找到它的实现源码代码并不多我们就全部粘贴出来

private class HostServiceProvider : IServiceProvider
{
private readonly WebHostBuilderContext _context;
public HostServiceProvider(WebHostBuilderContext context)
{
_context = context;
}

public object GetService(Type serviceType)
{
// 通过这里我们就比较清晰的看出,只有满足这几种情况下才能返回具体的实例,其他的都会返回null
#pragma warning disable CS0618 // Type or member is obsolete
if (serviceType == typeof(Microsoft.Extensions.Hosting.IHostingEnvironment)
|| serviceType == typeof(Microsoft.AspNetCore.Hosting.IHostingEnvironment)
#pragma warning restore CS0618 // Type or member is obsolete
|| serviceType == typeof(IWebHostEnvironment)
|| serviceType == typeof(IHostEnvironment)
)
{
return _context.HostingEnvironment;
}
if (serviceType == typeof(IConfiguration))
{
return _context.Configuration;
}
//不满足这几种情况的类型都返回null
return null;
}
}

通过这个内部私有类我们就能清晰的看到为何Starup的构造函数只能注入IWebHostEnvironment、IHostEnvironment、IConfiguration相关实例了,HostServiceProvider类实现了IServiceProvider的GetService方法并做了判断,只有满足这几种类型才能返回具体的实例注入,其它不满足条件的类型都会返回null。因此在初始化Starup实例的时候,通过构造函数注入的类型也就只能是这几种了。最终通过这个构造函数初始化了Startup类的实例。

ConfigureServices的装载

接下来我们就来在UseStartup方法里继续查看是如何查找并执行ConfigureServices方法的,继续查看找到如下实现

//传递startupType和环境变量参数查找返回ConfigureServicesBuilder
var configureServicesBuilder = StartupLoader.FindConfigureServicesDelegate(startupType, context.HostingEnvironment.EnvironmentName);
//调用Build方法返回ConfigureServices委托
var configureServices = configureServicesBuilder.Build(instance);
//传递services对象即IServiceCollection对象调用ConfigureServices方法
configureServices(services);

从上述代码中我们可以了解到查找并执行ConfigureServices方法的具体步骤可分为三步,首先在startupType类型中根据环境变量名称查找具体方法返回ConfigureServicesBuilder实例,然后构建ConfigureServicesBuilder实例返回ConfigureServices方法的委托,最后传递IServiceCollection对象执行委托方法。接下来我们就来查看具体实现源码。

我们在StartupLoader类中找到了FindConfigureServicesDelegate方法的相关实现

internal static ConfigureServicesBuilder FindConfigureServicesDelegate(Type startupType, string environmentName)
{
//根据startupType和根据environmentName构建的Configure{0}Services字符串先去查找返回类型为IServiceProvider的方法
//找不到在查找返回值为void类型的方法
var servicesMethod = FindMethod(startupType, “Configure{0}Services”, environmentName, typeof(IServiceProvider), required: false)
?? FindMethod(startupType, “Configure{0}Services”, environmentName, typeof(void), required: false);
//根据查找的到的MethodInfo去构建ConfigureServicesBuilder实例
return new ConfigureServicesBuilder(servicesMethod);
}

通过这里的源码我们可以看到在startupType类型里去查找名字为environmentName构建的Configure{0}Services的方法信息,然后根据查找的方法信息即MethodInfo对象去构建ConfigureServicesBuilder实例。接下里我们就来查询FindMethod方法的实现

private static MethodInfo FindMethod(Type startupType, string methodName, string environmentName, Type returnType = null, bool required = true)
{
//包含环境变量的ConfigureServices方法名称比如(ConfigureDevelopmentServices)
var methodNameWithEnv = string.Format(CultureInfo.InvariantCulture, methodName, environmentName);
//名为ConfigureServices的方法
var methodNameWithNoEnv = string.Format(CultureInfo.InvariantCulture, methodName, “”);
//方法是共有的静态的或非静态的方法
var methods = startupType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags Static);
//查找包含环境变量的ConfigureServices方法名称
var selectedMethods = methods.Where(method => method.Name.Equals(methodNameWithEnv, StringComparison.OrdinalIgnoreCase)).ToList();
if (selectedMethods.Count > 1)
{
//找打多个满足规则的方法直接抛出异常
throw new InvalidOperationException(string.Format(“Having multiple overloads of method {0} is not supported.”, methodNameWithEnv));

}
//如果不存在包含环境变量的ConfigureServices的方法比如(ConfigureDevelopmentServices),则直接查找方法名为ConfigureServices的方法
if (selectedMethods.Count == 0)
{
selectedMethods = methods.Where(method => method.Name.Equals(methodNameWithNoEnv, StringComparison.OrdinalIgnoreCase)).ToList();
//如果存在多个则同样抛出异常
if (selectedMethods.Count > 1)
{
throw new InvalidOperationException(string.Format(“Having multiple overloads of method {0} is not supported.”, methodNameWithNoEnv));
}
}

var methodInfo = selectedMethods.FirstOrDefault();
//如果没找到满足规则的方法,并且满足required参数,则抛出未找到方法的异常
if (methodInfo == null)
{
if (required)
{
throw new InvalidOperationException(string.Format(“A public method named {0} or {1} could not be found in the {2} type.”,
methodNameWithEnv,
methodNameWithNoEnv,
startupType.FullName));

}
return null;
}
//如果找到了名称一致的方法,但是返回类型和预期的不一致,也抛出异常
if (returnType != null && methodInfo.ReturnType != returnType)
{
if (required)
{
throw new InvalidOperationException(string.Format(“The {0} method in the type {1} must have a return type of {2}.”,
methodInfo.Name,
startupType.FullName,
returnType.Name));
}
return null;
}
return methodInfo;
}

通过FindMethod方法我们可以得到几个结论,首先ConfigureServices方法的名称可以是包含环境变量的名称比如(ConfigureDevelopmentServices),其次方法可以为共有的静态或非静态方法。FindMethod方法是真正执行查找的逻辑所在,如果找到相关方法则返回MethodInfo。FindMethod查找的方法名称是通过methodName参数传递进来的,我们标注的注释代码都是直接写死了ConfigureServices方法,只是为了便于说明理解,但其实FindMethod是通用方法,接下来要讲解的内容还会涉及到这个方法,到时候关于这个代码的逻辑我们就不会在进行说明了,因为是同一个方法,希望大家能注意到这一点。

通过上面的相关方法,我们了解到是通过什么样的规则去查找到ConfigureServices的方法信息的,我们也看到了ConfigureServicesBuilder正是通过查找到的MethodInfo去构造实例的