探究ASP.NET Core Startup初始化

Startup的另类指定方式#

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

//判断是否配置了StartupAssembly参数if(!string.IsNullOrEmpty(webHostOptions.StartupAssembly)){try{//根据你配置的程序集去查找StartupvarstartupType = 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这个常量值,最后我们找到了的值为

publicstaticreadonlystringStartupAssemblyKey ="startupAssembly";

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

publicstaticIHostBuilderCreateHostBuilder(string[] args)=>Host.CreateDefaultBuilder(args).ConfigureHostConfiguration(config=> {List> keyValuePairs =newList>();//配置Startup所在的程序集名称keyValuePairs.Add(newKeyValuePair("startupAssembly","Startup所在的程序集名称"));config.AddInMemoryCollection(keyValuePairs);}).ConfigureWebHostDefaults(webBuilder =>{//这样的话这里就可以省略了//webBuilder.UseStartup();});

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

publicstaticTypeFindStartupType(stringstartupAssemblyName,stringenvironmentName){varassembly = Assembly.Load(newAssemblyName(startupAssemblyName));//名称Startup+环境变量的类比如(StartupDevelopment)varstartupNameWithEnv ="Startup"+ environmentName;//名称为Startup的类varstartupNameWithoutEnv ="Startup";// 先查找包含名称Startup+环境变量的相关类,如果找不到则查找名称为Startup的类vartype =assembly.GetType(startupNameWithEnv) ??assembly.GetType(startupAssemblyName +"."+ startupNameWithEnv) ??assembly.GetType(startupNameWithoutEnv) ??assembly.GetType(startupAssemblyName +"."+ startupNameWithoutEnv);if(type ==null){// 如果上述规则找不到,则在程序集定义的所有类中继续查找vardefinedTypes = assembly.DefinedTypes.ToList();varstartupType1 = definedTypes.Where(info => info.Name.Equals(startupNameWithEnv, StringComparison.OrdinalIgnoreCase));varstartupType2 = definedTypes.Where(info => info.Name.Equals(startupNameWithoutEnv, StringComparison.OrdinalIgnoreCase));vartypeInfo = startupType1.Concat(startupType2).FirstOrDefault();if(typeInfo !=null){type = typeInfo.AsType();}}//最终返回Startup类型returntype;}

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

Startup的构造函数#

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

//创建Startup实例objectinstance = ActivatorUtilities.CreateInstance(newHostServiceProvider(webHostBuilderContext), startupType);

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

privateclassHostServiceProvider:IServiceProvider{privatereadonlyWebHostBuilderContext _context;publicHostServiceProvider(WebHostBuilderContext context){_context = context;}publicobjectGetService(Type serviceType){// 通过这里我们就比较清晰的看出,只有满足这几种情况下才能返回具体的实例,其他的都会返回null#pragmawarningdisable CS0618 // Type or member is obsoleteif(serviceType ==typeof(Microsoft.Extensions.Hosting.IHostingEnvironment)|| serviceType ==typeof(Microsoft.AspNetCore.Hosting.IHostingEnvironment)#pragmawarningrestore CS0618 // Type or member is obsolete|| serviceType ==typeof(IWebHostEnvironment)|| serviceType ==typeof(IHostEnvironment)){return_context.HostingEnvironment;}if(serviceType ==typeof(IConfiguration)){return_context.Configuration;}//不满足这几种情况的类型都返回nullreturnnull;}}

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

ConfigureServices的装载#

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

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

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

我们在StartupLoader类中找到了FindConfigureServicesDelegate方法的相关实现[点击查看源码]

internalstaticConfigureServicesBuilderFindConfigureServicesDelegate(Type startupType,stringenvironmentName){//根据startupType和根据environmentName构建的Configure{0}Services字符串先去查找返回类型为IServiceProvider的方法//找不到在查找返回值为void类型的方法varservicesMethod = FindMethod(startupType,"Configure{0}Services", environmentName,typeof(IServiceProvider), required:false)?? FindMethod(startupType,"Configure{0}Services", environmentName,typeof(void), required:false);//根据查找的到的MethodInfo去构建ConfigureServicesBuilder实例returnnewConfigureServicesBuilder(servicesMethod);}

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

privatestaticMethodInfoFindMethod(Type startupType,stringmethodName,stringenvironmentName, Type returnType =null,boolrequired =true){//包含环境变量的ConfigureServices方法名称比如(ConfigureDevelopmentServices)varmethodNameWithEnv =string.Format(CultureInfo.InvariantCulture, methodName, environmentName);//名为ConfigureServices的方法varmethodNameWithNoEnv =string.Format(CultureInfo.InvariantCulture, methodName,"");//方法是共有的静态的或非静态的方法varmethods = startupType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);//查找包含环境变量的ConfigureServices方法名称varselectedMethods = methods.Where(method => method.Name.Equals(methodNameWithEnv, StringComparison.OrdinalIgnoreCase)).ToList();if(selectedMethods.Count >1){//找打多个满足规则的方法直接抛出异常thrownewInvalidOperationException(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){thrownewInvalidOperationException(string.Format("Having multiple overloads of method '{0}' is not supported.", methodNameWithNoEnv));}}varmethodInfo = selectedMethods.FirstOrDefault();//如果没找到满足规则的方法,并且满足required参数,则抛出未找到方法的异常if(methodInfo ==null){if(required){thrownewInvalidOperationException(string.Format("A public method named '{0}' or '{1}' could not be found in the '{2}' type.",methodNameWithEnv,methodNameWithNoEnv,startupType.FullName));}returnnull;}//如果找到了名称一致的方法,但是返回类型和预期的不一致,也抛出异常if(returnType !=null&& methodInfo.ReturnType != returnType){if(required){thrownewInvalidOperationException(string.Format("The '{0}' method in the type '{1}' must have a return type of '{2}'.",methodInfo.Name,startupType.FullName,returnType.Name));}returnnull;}returnmethodInfo;}

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

通过上面的相关方法,我们了解到了是通过什么样的规则去查找到ConfigureServices的方法信息的,我们也看到了ConfigureServicesBuilder正是通过查找到的MethodInfo去构造实例的,接下来我们就来查看下ConfigureServicesBuilder的实现源码[点击查看源码]

internalclassConfigureServicesBuilder{//构造函数传递的configureServices的MethodInfopublicConfigureServicesBuilder(MethodInfo configureServices){MethodInfo = configureServices;}publicMethodInfo MethodInfo {get; }publicFunc, Func> StartupServiceFilters {get;set; } = f => f;//Build委托publicFuncBuild(objectinstance)=> services => Invoke(instance, services);privateIServiceProviderInvoke(objectinstance, IServiceCollection services){//执行StartupServiceFilters委托参数为Func类型的委托方法即Startup//返回了Func委托,执行这个委托需传递services即IServiceCollections实例返回IServiceProvider类型returnStartupServiceFilters(Startup)(services);IServiceProviderStartup(IServiceCollection serviceCollection)=> InvokeCore(instance, serviceCollection);}privateIServiceProviderInvokeCore(objectinstance, IServiceCollection services){if(MethodInfo ==null){returnnull;}// 如果ConfigureServices方法包含多个参数或方法参数类型不是IServiceCollection类型则直接抛出异常// 也就是说ConfigureServices只能包含一个参数且类型为IServiceCollectionvarparameters = MethodInfo.GetParameters();if(parameters.Length >1||parameters.Any(p => p.ParameterType !=typeof(IServiceCollection))){thrownewInvalidOperationException("The ConfigureServices method must either be parameterless or take only one parameter of type IServiceCollection.");}//找到ConfigureServices方法的参数,并将services即IServiceCollection的实例传递给这个参数vararguments =newobject[MethodInfo.GetParameters().Length];if(parameters.Length >0){arguments[0] = services;}// 执行返回IServiceProvider实例returnMethodInfo.InvokeWithoutWrappingExceptions(instance, arguments)asIServiceProvider;}}

看完ConfigureServicesBuilder类的实现逻辑,关于通过什么样的逻辑查找并执行ConfigureServices方法的逻辑就非常清晰了。首先是查找ConfigureServices方法,即包含环境变量的ConfigureServices方法名称比如(ConfigureDevelopmentServices)或名为ConfigureServices的方法,返回的是ConfigureServicesBuilder对象。然后执行ConfigureServicesBuilder的Build方法,这个方法里包含了执行ConfigureServices的规则,即ConfigureServices只能包含一个参数且类型为IServiceCollection,然后将当前程序中存在的IServiceCollection实例传递给它。

Configure的装载#

我们常使用Startup的Configure方法去配置中间件,默认生成的Configure方法为我们添加了IApplicationBuilder和IWebHostEnvironment实例,但是其实Configure方法不仅仅可以传递这两个参数,它可以通过参数注入在IServiceCollection中注册的所有服务,究竟是如何实现的呢,接下来我们继续探究UseStartup方法查找源码查看想实现

[点击查看源码],我们抽离出来核心实现如下

//和ConfigureServices查找方式类似传递Startup实例和环境变量ConfigureBuilder configureBuilder = StartupLoader.FindConfigureDelegate(startupType, context.HostingEnvironment.EnvironmentName);services.Configure(options =>{//通过查看GenericWebHostServiceOptions的源码可知app其实就是IApplicationBuilder实例options.ConfigureApplication = app =>{startupError?.Throw();//执行Startup.Configure,instance为Startup实例if(instance !=null&& configureBuilder !=null){//执行Configure方法传递Startup实例和IApplicationBuilder实例configureBuilder.Build(instance)(app);}};});

我们通过查看GenericWebHostServiceOptions的源码可知ConfigureApplication属性的类型为Action也就是说app参数其实就是IApplicationBuilder接口的实例。通过上面这段代码可以看出,主要逻辑就是调用StartupLoader的FindConfigureDelegate方法,然后返回ConfigureBuilder建造类,然后构建出Configure方法并执行。首先我们来查看FindConfigureDelegate的逻辑实现

[点击查看源码]

internalstaticConfigureBuilderFindConfigureDelegate(Type startupType,stringenvironmentName){//通过startup类型和方法名为Configure或Configure+环境变量名称的方法varconfigureMethod = FindMethod(startupType,"Configure{0}", environmentName,typeof(void), required:true);//用查找到的方法去初始化ConfigureBuilderreturnnewConfigureBuilder(configureMethod);}

从这里我们可以看到FindConfigureDelegate方法也是调用的FindMethod方法,只是传递的方法名字符串为Configure或Configure+环境变量,关于FindMethod的方法实现我们在上面讲解ConfigureServices方法的时候已经非常详细的说过了,这里就不过多的讲解了。总之是通过FindMethod去查找名为Configure的方法或名为Configure+环境变量的方法比如ConfigureDevelopment查找规则和ConfigureServices是完全一致的。但是Configure方法却可以通过参数注入注册到IServiceCollection中的服务,答案我们同样要在ConfigureBuilder类中去探寻

[点击查看源码]

internalclassConfigureBuilder{//构造函数传递Configure的MethodInfopublicConfigureBuilder(MethodInfo configure){MethodInfo = configure;}publicMethodInfo MethodInfo {get; }//Build方法返回Action委托publicActionBuild(objectinstance)=> builder => Invoke(instance, builder);//执行逻辑privatevoidInvoke(objectinstance, IApplicationBuilder builder){//通过IApplicationBuilder的ApplicationServices获取IServiceProvider实例创建一个作用域using(varscope = builder.ApplicationServices.CreateScope()){//获取IServiceProvider实例varserviceProvider = scope.ServiceProvider;//获取Configure的所有参数varparameterInfos = MethodInfo.GetParameters();varparameters =newobject[parameterInfos.Length];for(varindex =0; index < parameterInfos.Length; index++){varparameterInfo = parameterInfos[index];//如果方法参数为IApplicationBuilder类型则直接将传递过来的IApplicationBuilder赋值给它if(parameterInfo.ParameterType ==typeof(IApplicationBuilder)){parameters[index] = builder;}else{try{//根据方法的参数类型在serviceProvider中获取具体实例赋值给对应参数parameters[index] = serviceProvider.GetRequiredService(parameterInfo.ParameterType);}catch (Exception ex){//如果对应的方法参数名称,没在serviceProvider中获取到则直接抛出异常//变相的说明了Configure方法的参数必须是注册在IServiceCollection中的}}}MethodInfo.InvokeWithoutWrappingExceptions(instance, parameters);}}}

通过ConfigureBuilder类的实现逻辑,可以清晰的看到为何Configure方法参数可以注入任何在IServiceCollection中注册的服务了。接下来我们总结一下Configure方法的初始化逻辑,首先在Startup中查找方法名为Configure或Configure+环境变量名称(比如ConfigureDevelopment)的方法,然后查找IApplicationBuilder类型的参数,如果找到则将程序中的IApplicationBuilder实例传递给它。至于为何Configure方法能够通过参数注入任何在IServiceCollection中注册的服务,则是因为循环Configure中的所有参数然后在IOC容器中获取对应实例赋值过来,Configure方法的参数一定得是在IServiceCollection注册过的类型,否则会抛出异常。

ConfigureContainer为何会被调用#

如果你在ASP.NET Core 3.1中使用过Autofac那么你对ConfigureContainer方法一定不陌生,它和ConfigureServices、Configure方法一样的神奇,在几乎没有任何约束的情况下我们只需要定义ConfigureContainer方法并为方法传递一个ContainerBuilder参数,那么这个方法就能顺利的被调用了。这一切究竟是如何实现的呢,接下来我们继续探究源码,找到了如下的逻辑

[点击查看源码]

//根据规则查找最终返回ConfigureContainerBuilder实例varconfigureContainerBuilder = StartupLoader.FindConfigureContainerDelegate(startupType, context.HostingEnvironment.EnvironmentName);if(configureContainerBuilder.MethodInfo !=null){//获取容器类型比如如果是autofac则类型为ContainerBuildervarcontainerType = configureContainerBuilder.GetContainerType();// 存储configureContainerBuilder实例_builder.Properties[typeof(ConfigureContainerBuilder)] = configureContainerBuilder;//构建一个Action类型的委托varactionType =typeof(Action<,>).MakeGenericType(typeof(HostBuilderContext), containerType);// 获取此类型的私有ConfigureContainer方法,然后声明该方法的泛型为容器类型,然后创建这个方法的委托varconfigureCallback = GetType().GetMethod(nameof(ConfigureContainer), BindingFlags.NonPublic | BindingFlags.Instance).MakeGenericMethod(containerType).CreateDelegate(actionType,this);// 等同于执行_builder.ConfigureContainer(ConfigureContainer),其中T为容器类型。//C onfigureContainer表示一个委托,即我们在Startup中定义的ConfigureContainer委托typeof(IHostBuilder).GetMethods().First(m => m.Name ==nameof(IHostBuilder.ConfigureContainer)).MakeGenericMethod(containerType).InvokeWithoutWrappingExceptions(_builder,newobject[] { configureCallback });}

继续使用老配方,我们查看StartupLoader的FindConfigureContainerDelegate方法实现

[点击查看源码]

internalstaticConfigureContainerBuilderFindConfigureContainerDelegate(Type startupType,stringenvironmentName){//根据startupType和根据environmentName构建的Configure{0}Services字符串先去查找返回类型为IServiceProvider的方法varconfigureMethod = FindMethod(startupType,"Configure{0}Container", environmentName,typeof(void), required:false);//用查找到的方法去初始化ConfigureContainerBuilderreturnnewConfigureContainerBuilder(configureMethod);}

果然还是这个配方这个味道,废话不多说直接查看ConfigureContainerBuilder源码

[点击查看源码]

internalclassConfigureContainerBuilder{publicConfigureContainerBuilder(MethodInfo configureContainerMethod){MethodInfo = configureContainerMethod;}publicMethodInfo MethodInfo {get; }publicFunc, Action> ConfigureContainerFilters {get;set; } = f => f;publicActionBuild(objectinstance)=> container => Invoke(instance, container);//查找容器类型,其实就是ConfigureContainer方法的的唯一参数publicTypeGetContainerType(){varparameters = MethodInfo.GetParameters();//ConfigureContainer方法只能包含一个参数if(parameters.Length !=1){thrownewInvalidOperationException($"The{MethodInfo.Name}method must take only one parameter.");}returnparameters[0].ParameterType;}privatevoidInvoke(objectinstance,objectcontainer){ConfigureContainerFilters(StartupConfigureContainer)(container);voidStartupConfigureContainer(objectcontainerBuilder)=> InvokeCore(instance, containerBuilder);}//根据传递的container对象执行ConfigureContainer方法逻辑比如使用autofac时ConfigureContainer(ContainerBuilder)privatevoidInvokeCore(objectinstance,objectcontainer){if(MethodInfo ==null){return;}vararguments =newobject[1] { container };MethodInfo.InvokeWithoutWrappingExceptions(instance, arguments);}}

果不其然千年老方下来还是那个味道,和ConfigureServices、Configure方法思路几乎一致。这里需要注意的是GetContainerType获取的容器类型是ConfigureContainer方法的唯一参数即容器类型,如果传递多个参数则直接抛出异常。其实Startup的ConfigureContainer方法经过花里胡哨的一番操作之后,最终还是转换成了雷士如下的操作方式,这个我们在上面代码中构建actionType的时候就可以看出,最终通过查找到的容器类型去完成注册等相关操作,这里就不过多的讲解了

Host.CreateDefaultBuilder(args).ConfigureContainer((context,container)=> {container.RegisterType().As().InstancePerLifetimeScope();});

亚马逊测评 www.yisuping.com

你可能感兴趣的:(探究ASP.NET Core Startup初始化)