写在前面
开发.NET Core应用,直接映入眼帘的就是Startup
类和Program
类,它们是.NET Core
应用程序的起点。通过使用Startup
,可以配置化处理所有向应用程序所做的请求的管道,同时也可以减少.NET
应用程序对单一服务器的依赖性,使我们在更大程度上专注于面向多服务器为中心的开发模式。
Startup
讨论
Starup
所承担的角色
Startup
作为一个概念是ASP.NET Core
程序中所必须的,Startup
类本身可以使用多种修饰符(public、protect,private、internal
),作为ASP.NET Core应用程序的入口,它包含与应用程序相关配置的功能或者说是接口。
虽然在程序里我们使用的类名就是Startup
,但是需要注意的是,Startup
是一个抽象概念,你完全可以名称成其他的,比如MyAppStartup
或者其他的什么名称,只要你在Program
类中启动你所定义的启动类即可。
当然如果不想写Startup
,可以在Program
类中配置服务和请求处理管道,请参见评论区5楼,非常感谢Emrys耐心而又全面的指正。
以下是基于ASP.NET Core Preview 3
模板中提供的写法:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
不管你命名成什么,只要将webBuilder.UseStartup<>()
中的泛型类配置成你定义的入口类即可;
Startup
编写规范
下面是ASP.NET Core 3.0 Preview 3模板中Startup
的写法:
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting(routes =>
{
routes.MapControllers();
});
app.UseAuthorization();
}
通过以上代码可以知道,Startup
类中一般包括
- 构造函数:通过我们以前的开发经验,我们可以知道,该构造方法可以包括多个对象
IConfiguration
:表示一组键/值应用程序配置属性。IApplicationBuilder
:是一个包含与当前环境相关的属性和方法的接口。它用于获取应用程序中的环境变量。IHostingEnvironment
:是一个包含与运行应用程序的Web宿主环境相关信息的接口。使用这个接口方法,我们可以改变应用程序的行为。ILoggerFactory
:是为ASP.NET Core中的日志记录系统提供配置的接口。它还创建日志系统的实例。
ConfigureServices
Configure
Startup
在创建服务时,会执行依赖项注册服务,以便在应用程序的其它地方使用这些依赖项。ConfigureServices
用于注册服务,Configure
方法允许我们向HTTP管道添加中间件和服务。这就是ConfigureServices
先于Configure
之前调用的原因。
ConfigureServices
该方法时可选的,非强制约束,它主要用于对依赖注入或ApplicationServices
在整个应用中的支持,该方法必须是public
的,其典型模式是调用所有 Add{Service}
方法,主要场景包括实体框架、认证和 MVC 注册服务:
services.AddDbContext<ApplicationDbContext>(options =>options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>().AddDefaultUI(UIFramework.Bootstrap4).AddEntityFrameworkStores<ApplicationDbContext>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// Add application services.此处主要是注册IOC服务
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
Configure
该方法主要用于定义应用程序对每个HTTP请求的响应方式,即我们可以控制ASP.NET管道,还可用于在HTTP管道中配置中间件。请求管道中的每个中间件组件负责调用管道中的下一个组件,或在适当情况下使链发生短路。 如果中间件链中未发生短路,则每个中间件都有第二次机会在将请求发送到客户端前处理该请求。
该方法接受IApplicationBuilder
作为参数,同时还可以接收其他一些可选参数,如IHostingEnvironment
和ILoggerFactory
。
一般而言,只要将服务注册到configureServices
方法中时,都可以在该方法中使用。
app.UseDeveloperExceptionPage();
app.UseHsts();
app.UseHttpsRedirection();
app.UseRouting(routes =>
{
routes.MapControllers();
});
app.UseAuthorization();
扩展Startup
方法
使用IStartupFilter
来对Startup
功能进行扩展,在应用的Configure
中间件管道的开头或末尾使用IStartupFilter
来配置中间件。IStartupFilter
有助于确保当库在应用请求处理管道的开端或末尾添加中间件的前后运行中间件。
以下是IStartupFilter
的源代码,通过源代码我们可以知道,该接口有一个Action<IApplicationBuilder>
类型,并命名为Configure
的方法。由于传入参数类型和返回类型一样,这就保证了扩展的传递性及顺序性,具体的演示代码,可以参数MSDN
using System;
using Microsoft.AspNetCore.Builder;
namespace Microsoft.AspNetCore.Hosting
{
public interface IStartupFilter
{
Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next);
}
}
Startup
是如何注册和执行的
此段文字,只是我想深入了解其内部机制而写的,如果本身也不了解,其实是不影响我们正常编写.NET Core应用的。
UseStartup
源码
ASP.NET Core通过调用IWebHostBuilder.UseStartup
方法,传入Startup
类型,注意开篇就已经说过Startup
是一个抽象概念,我们看下源代码:
/// <summary>
/// Specify the startup type to be used by the web host.
/// </summary>
/// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param>
/// <param name="startupType">The <see cref="Type"/> to be used.</param>
/// <returns>The <see cref="IWebHostBuilder"/>.</returns>
public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType)
{
var startupAssemblyName = startupType.GetTypeInfo().Assembly.GetName().Name;
hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName);
// Light up the GenericWebHostBuilder implementation
if (hostBuilder is ISupportsStartup supportsStartup)
{
return supportsStartup.UseStartup(startupType);
}
return hostBuilder
.ConfigureServices(services =>
{
if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo()))
{
services.AddSingleton(typeof(IStartup), startupType);
}
else
{
services.AddSingleton(typeof(IStartup), sp =>
{
var hostingEnvironment = sp.GetRequiredService<IHostEnvironment>();
return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName));
});
}
});
}
/// <summary>
/// Specify the startup type to be used by the web host.
/// </summary>
/// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param>
/// <typeparam name ="TStartup">The type containing the startup methods for the application.</typeparam>
/// <returns>The <see cref="IWebHostBuilder"/>.</returns>
public static IWebHostBuilder UseStartup<TStartup>(this IWebHostBuilder hostBuilder) where TStartup : class
{
return hostBuilder.UseStartup(typeof(TStartup));
}
创建Startup
实例
/// <summary>
/// Adds a delegate for configuring additional services for the host or web application. This may be called
/// multiple times.
/// </summary>
/// <param name="configureServices">A delegate for configuring the <see cref="IServiceCollection"/>.</param>
/// <returns>The <see cref="IWebHostBuilder"/>.</returns>
public IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices)
{
if (configureServices == null)
{
throw new ArgumentNullException(nameof(configureServices));
}
return ConfigureServices((_, services) => configureServices(services));
}
/// <summary>
/// Adds a delegate for configuring additional services for the host or web application. This may be called
/// multiple times.
/// </summary>
/// <param name="configureServices">A delegate for configuring the <see cref="IServiceCollection"/>.</param>
/// <returns>The <see cref="IWebHostBuilder"/>.</returns>
public IWebHostBuilder ConfigureServices(Action<WebHostBuilderContext, IServiceCollection> configureServices)
{
_configureServices += configureServices;
return this;
}
关于ConfigureServices
的定义及注册方式,是在IWebHostBuilder.ConfigureServices
实现的,同时可以注意一下25行代码,向大家说明了多次注册Startup
的ConfigureServices
方法时,会合并起来的根源。此处抽象委托用的也非常多。
该类里面还有Build
方法,我就不贴出代码了,只需要知道,主进程在此处开始了。接下来一个比较重要的方法,是BuildCommonServices
,它向当前ServiceCollection
中添加一些公共框架级服务,以下是部分代码,具体代码请查看WebHostBuilder
。
try
{
var startupType = StartupLoader.FindStartupType(_options.StartupAssembly, _hostingEnvironment.EnvironmentName);
if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo()))
{
services.AddSingleton(typeof(IStartup), startupType);
}
else
{
services.AddSingleton(typeof(IStartup), sp =>
{
var hostingEnvironment = sp.GetRequiredService<IHostEnvironment>();
var methods = StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName);
return new ConventionBasedStartup(methods);
});
}
}
catch (Exception ex)
{
var capture = ExceptionDispatchInfo.Capture(ex);
services.AddSingleton<IStartup>(_ =>
{
capture.Throw();
return null;
});
}
由此可见,如果我们的Startup
类直接实现IStartup
,它可以并且将直接注册为IStartup
的实现类型。只不过ASP.NET Core模板代码并没有实现IStartup
,它更多的是一种约定,并通过DI
调用委托,依此调用Startup
内的构造函数还有另外两个方法。
同时上述代码还展示了如何创建Startup
类型,就是用到了静态方法StartupLoader.LoadMethods
类生成StartupMethods
实例。
ConfigureServices
和Configure
当WebHost
初始化时,框架会去查找相应的方法,这里,我们主要查看源代码,其中的核心方法是StartupLoader.FindMethods
private static MethodInfo FindMethod(Type startupType, string methodName, string environmentName, Type returnType = null, bool required = true)
{
var methodNameWithEnv = string.Format(CultureInfo.InvariantCulture, methodName, environmentName);
var methodNameWithNoEnv = string.Format(CultureInfo.InvariantCulture, methodName, "");
var methods = startupType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
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));
}
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();
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;
}
它查找的第一个委托是ConfigureDelegate
,该委托将用于构建应用程序的中间件管道。FindMethod
完成了大部分工作,具体的代码请查看StartupLoader
。此方法根据传递给它的methodName
参数在Startup
类中查找响应的方法。
我们知道,Startup
的定义更多的是约定,所以会去查找Configure
和ConfigureServices
。当然,通过源代码我还知道,除了提供标准的“Configure
”方法之外,我们还可以通过环境配置找到响应的Configure
和ConfigureServices
。根本来说,我们最终查找到的是ConfigureContainerDelegate
。
接下来,一个比较重要的方法是LoadMethods
public static StartupMethods LoadMethods(IServiceProvider hostingServiceProvider, Type startupType, string environmentName)
{
var configureMethod = FindConfigureDelegate(startupType, environmentName);
var servicesMethod = FindConfigureServicesDelegate(startupType, environmentName);
var configureContainerMethod = FindConfigureContainerDelegate(startupType, environmentName);
object instance = null;
if (!configureMethod.MethodInfo.IsStatic || (servicesMethod != null && !servicesMethod.MethodInfo.IsStatic))
{
instance = ActivatorUtilities.GetServiceOrCreateInstance(hostingServiceProvider, startupType);
}
// The type of the TContainerBuilder. If there is no ConfigureContainer method we can just use object as it's not
// going to be used for anything.
var type = configureContainerMethod.MethodInfo != null ? configureContainerMethod.GetContainerType() : typeof(object);
var builder = (ConfigureServicesDelegateBuilder) Activator.CreateInstance(
typeof(ConfigureServicesDelegateBuilder<>).MakeGenericType(type),
hostingServiceProvider,
servicesMethod,
configureContainerMethod,
instance);
return new StartupMethods(instance, configureMethod.Build(instance), builder.Build());
}
该方法通过查找对应的方法,由于Startup
并未在DI
中注册,所以会调用GetServiceOrCreateInstance
创建一个Startup
实例,此时构造函数也在此得到解析。
通过一系列的调用,最终到达了ConfigureServicesBuilder.Invoke
里面。Invoke
方法使用反射来获取和检查在Startup
类上定义的ConfigureServices
方法所需的参数。
private IServiceProvider InvokeCore(object instance, IServiceCollection services)
{
if (MethodInfo == null)
{
return null;
}
// Only support IServiceCollection parameters
var parameters = MethodInfo.GetParameters();
if (parameters.Length > 1 ||
parameters.Any(p => p.ParameterType != typeof(IServiceCollection)))
{
throw new InvalidOperationException("The ConfigureServices method must either be parameterless or take only one parameter of type IServiceCollection.");
}
var arguments = new object[MethodInfo.GetParameters().Length];
if (parameters.Length > 0)
{
arguments[0] = services;
}
return MethodInfo.Invoke(instance, arguments) as IServiceProvider;
}
最后我们来看一下ConfigureBuilder
类,它需要一个Action<IApplicationBuilder>
委托变量,其中包含每个IStartupFilter
的一组包装的Configure
方法,最后一个是Startup.Configure
方法的委托。此时,所调用的配置链首先命中的是AutoRequestServicesStartupFilter.Configure
方法。并将该委托链作为下一个操作,之后会调用ConventionBasedStartup.Configure
方法。这将在其本地StartupMethods
对象上调用ConfigureDelegate
。
private void Invoke(object instance, IApplicationBuilder builder)
{
// Create a scope for Configure, this allows creating scoped dependencies
// without the hassle of manually creating a scope.
using (var scope = builder.ApplicationServices.CreateScope())
{
var serviceProvider = scope.ServiceProvider;
var parameterInfos = MethodInfo.GetParameters();
var parameters = new object[parameterInfos.Length];
for (var index = 0; index < parameterInfos.Length; index++)
{
var parameterInfo = parameterInfos[index];
if (parameterInfo.ParameterType == typeof(IApplicationBuilder))
{
parameters[index] = builder;
}
else
{
try
{
parameters[index] = serviceProvider.GetRequiredService(parameterInfo.ParameterType);
}
catch (Exception ex)
{
throw new Exception(string.Format(
"Could not resolve a service of type '{0}' for the parameter '{1}' of method '{2}' on type '{3}'.",
parameterInfo.ParameterType.FullName,
parameterInfo.Name,
MethodInfo.Name,
MethodInfo.DeclaringType.FullName), ex);
}
}
}
MethodInfo.Invoke(instance, parameters);
}
}
Startup.Configure
方法会调用ServiceProvider
所解析的相应的参数,该方法还可以使用IApplicationBuilder
将中间件添加到应用程序管道中。最终的RequestDelegate
是从IApplicationBuilder
构建并返回的,至此WebHost
初始化完成。