一個典型的ASP.NET Core應用程式會包含Program與Startup兩個文件。Program類中有應用程式的入口方法Main,其中的處理邏輯通常是創建一個WebHostBuilder,再生成WebHost,最後啟動之。 而在創建WebHostBuilder時又會常常會指定一個Startup ...
一個典型的ASP.NET Core應用程式會包含Program與Startup兩個文件。Program類中有應用程式的入口方法Main,其中的處理邏輯通常是創建一個WebHostBuilder,再生成WebHost,最後啟動之。
而在創建WebHostBuilder時又會常常會指定一個Startup類型。
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
這個Startup類里究竟做了哪些事情呢?
查一下UseStartup方法的實現內容:
public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType)
{
var startupAssemblyName = startupType.GetTypeInfo().Assembly.GetName().Name;
return hostBuilder
.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName)
.ConfigureServices(services =>
{
if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo()))
{
services.AddSingleton(typeof(IStartup), startupType);
}
else
{
services.AddSingleton(typeof(IStartup), sp =>
{
var hostingEnvironment = sp.GetRequiredService<IHostingEnvironment>();
return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName));
});
}
});
}
可以看出所指定的Startup類型會在DI容器中註冊為單例形式,註冊的處理過程被封裝成Action
同時Startup類型可以有兩種實現方式:
- 自定義實現IStartup介面的類
- 內部定義的ConventionBasedStartup類
實際使用的Startup類經常是這樣的:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
}
}
所以明顯不是第一種方式,那麼其又是怎麼與第二種方式關聯起來的?
ConventionBasedStartup的構造方法中傳入了StartupMethods類型的參數,它的LoadMethod方法里曝露了更多的信息。
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());
}
它的內部處理中會找尋三種方法,ConfigureServices, Configure與ConfigureContainer。
private static ConfigureBuilder FindConfigureDelegate(Type startupType, string environmentName)
{
var configureMethod = FindMethod(startupType, "Configure{0}", environmentName, typeof(void), required: true);
return new ConfigureBuilder(configureMethod);
}
private static ConfigureContainerBuilder FindConfigureContainerDelegate(Type startupType, string environmentName)
{
var configureMethod = FindMethod(startupType, "Configure{0}Container", environmentName, typeof(void), required: false);
return new ConfigureContainerBuilder(configureMethod);
}
private static ConfigureServicesBuilder FindConfigureServicesDelegate(Type startupType, string environmentName)
{
var servicesMethod = FindMethod(startupType, "Configure{0}Services", environmentName, typeof(IServiceProvider), required: false)
?? FindMethod(startupType, "Configure{0}Services", environmentName, typeof(void), required: false);
return new ConfigureServicesBuilder(servicesMethod);
}
ConfigureServices方法用於在容器中註冊各種所需使用的服務介面類型,Configure方法中可以使用各種middleware(中間件),對HTTP請求pipeline(管道)進行配置。
這二者與IStartup介面的方法基本一致,而ConfigureContainer方法則是提供了一種引入第三方DI容器的功能。
public interface IStartup
{
IServiceProvider ConfigureServices(IServiceCollection services);
void Configure(IApplicationBuilder app);
}
使用第二種Startup類型的實現方式時,對於Configure方法的參數也可以更加靈活,不僅可以傳入IApplicationBuilder類型,還可以有其它已註冊過的任意介面類型。
ConfigureBuilder類的Build方法對額外參數的處理方法簡單明瞭,是IApplicationBuilder類型的直接傳入參數實例,不是的則從DI容器中獲取實例:
public class ConfigureBuilder
{
public Action<IApplicationBuilder> Build(object instance) => builder => Invoke(instance, builder);
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);
}
...
}
}
MethodInfo.Invoke(instance, parameters);
}
}
}
此外,在找尋各種方法的處理中可以看到環境變數的身影。所以用第二種方式可以依據不同的環境變數定義同類型但不同名稱的方法,這樣可以省去寫不少if...else...
的處理。
UseStartup方法中還只是申明瞭需要註冊Startup類型,實際的調用是在WebHostBuilder類執行Build方法時發生的。
private IServiceCollection BuildCommonServices(out AggregateException hostingStartupErrors)
{
...
foreach (var configureServices in _configureServicesDelegates)
{
configureServices(_context, services);
}
return services;
}
至於Startup類型中方法的調用時機,則需跟蹤到WebHost類中。
首先是它的Initialize方法會確保獲得Startup類的實例,並調用ConfigureServices方法註冊服務介面。
private void EnsureApplicationServices()
{
if (_applicationServices == null)
{
EnsureStartup();
_applicationServices = _startup.ConfigureServices(_applicationServiceCollection);
}
}
private void EnsureStartup()
{
if (_startup != null)
{
return;
}
_startup = _hostingServiceProvider.GetService<IStartup>();
...
}
然後WebHost實例被啟動時,它的BuildApplication方法會創建一個ApplicationBuilder實例,以其作為Configure方法參數,同時調用Configure方法,這時Configure方法中對那些middleware的處理方式(即Func<RequestDelegate, RequestDelegate>方法)會被加入到ApplicationBuilder之中。
private RequestDelegate BuildApplication()
{
try
{
_applicationServicesException?.Throw();
EnsureServer();
var builderFactory = _applicationServices.GetRequiredService<IApplicationBuilderFactory>();
var builder = builderFactory.CreateBuilder(Server.Features);
builder.ApplicationServices = _applicationServices;
var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>();
Action<IApplicationBuilder> configure = _startup.Configure;
foreach (var filter in startupFilters.Reverse())
{
configure = filter.Configure(configure);
}
configure(builder);
return builder.Build();
}
...
}
ApplicationBuilder的Build方法將這些處理邏輯嵌套起來,最底層的是返回404未找到的處理邏輯。
public RequestDelegate Build()
{
RequestDelegate app = context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
};
foreach (var component in _components.Reverse())
{
app = component(app);
}
return app;
}
BuildApplication方法返回已嵌套的RequestDelegate委托方法,併在之後生成的HostingApplication實例中,將其傳入它的構造方法。
public virtual async Task StartAsync(CancellationToken cancellationToken = default)
{
HostingEventSource.Log.HostStart();
_logger = _applicationServices.GetRequiredService<ILogger<WebHost>>();
_logger.Starting();
var application = BuildApplication();
_applicationLifetime = _applicationServices.GetRequiredService<IApplicationLifetime>() as ApplicationLifetime;
_hostedServiceExecutor = _applicationServices.GetRequiredService<HostedServiceExecutor>();
var diagnosticSource = _applicationServices.GetRequiredService<DiagnosticListener>();
var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();
var hostingApp = new HostingApplication(application, _logger, diagnosticSource, httpContextFactory);
await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);
...
}
上述方法中HostingApplication實例最終被傳入KestrelServer的啟動方法中,這樣才能在其內部調用HostingApplication的ProcessRequestAsync方法,並開始層層調用諸多的RequestDelegate方法。
public Task ProcessRequestAsync(Context context)
{
return _application(context.HttpContext);
}
如果覺得使用Startup類還是有點麻煩的話,直接使用WebHostBuilder所提供的擴展方法也是同樣的效果。
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
HostingEnvironment = hostingContext.HostingEnvironment;
Configuration = config.Build();
})
.ConfigureServices(services =>
{
services.AddMvc();
})
.Configure(app =>
{
if (HostingEnvironment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseMvcWithDefaultRoute();
app.UseStaticFiles();
});
不過使用獨立的Startup類有著額外的好處,Startup類可以被包含在與Program類不用的的程式集中。然後通過WebHostOptions類中StartupAssembly屬性的設定及其它相關處理,完成UseStartup方法同樣的功能。
private IServiceCollection BuildCommonServices(out AggregateException hostingStartupErrors)
{
...
if (!string.IsNullOrEmpty(_options.StartupAssembly))
{
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<IHostingEnvironment>();
var methods = StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName);
return new ConventionBasedStartup(methods);
});
}
}
...
}
...
return services;
}