本文介紹ASP.NET Core默認模板是如何完成初始化的,不多廢話,直入主題
首先使用默認模板創建一個ASP.NET Core程序
瞅一眼解決方案結構,包含了EF Core和Identity的默認實現,沒啥特別的
我們知道控制台程序的入口點在Main函數,ASP.NET Core也是一樣的。所以我們看Program文件中的Main函數,它長這樣
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace AspNet5
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
}
}
}
從以上代碼我們可以看到,Main
函數只調用了CreateHostBuilder
一個方法,這個方法利用Host
靜態類創建了一個IHostBuilder
供Main
函數Build and Run
,從build、builder
字樣和分析后不難得出這里使用了建造者模式
既然是建造者模式,就涉及到了建造“原料”。在這段代碼里,只調用了CreateDefaultBuilder
、ConfigureWebHostDefaults
和Build
三個方法。那么“原料”和建造過程就藏在這幾個方法里,我們一個一個來看
首先是CreateDefaultBuilder
方法,它完成了以下的事情,提供了“原料”
- 把
IHostEnvironment.ContentRootPath
的值設置為Directory.GetCurrentDirectory()
(當前工作目錄) - 使用前綴為
DOTNET_
的環境變量來加載Host
- 使用命令行輸入的參數
args
來加載Host
- 使用配置文件
appsettings.json
和當前運行環境對應的配置文件,如appsetting.Development.json
來加載程序 - 當前運行環境為
Development
並且存在和環境變量ApplicationName
匹配的入口程序集時,從程序集中加載User Secrets
,用其來加載程序 - 使用命令行輸入的參數
args
來加載程序 - 配置
ILoggerFactory
,用來輸出日志到命令行窗口、debug窗口、event source - 當前運行環境為
Development
時,啟用依賴注入容器的范圍驗證
/// <summary>
/// Initializes a new instance of the <see cref="HostBuilder"/> class with pre-configured defaults.
/// </summary>
/// <remarks>
/// The following defaults are applied to the returned <see cref="HostBuilder"/>:
/// <list type="bullet">
/// <item><description>set the <see cref="IHostEnvironment.ContentRootPath"/> to the result of <see cref="Directory.GetCurrentDirectory()"/></description></item>
/// <item><description>load host <see cref="IConfiguration"/> from "DOTNET_" prefixed environment variables</description></item>
/// <item><description>load host <see cref="IConfiguration"/> from supplied command line args</description></item>
/// <item><description>load app <see cref="IConfiguration"/> from 'appsettings.json' and 'appsettings.[<see cref="IHostEnvironment.EnvironmentName"/>].json'</description></item>
/// <item><description>load app <see cref="IConfiguration"/> from User Secrets when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development' using the entry assembly</description></item>
/// <item><description>load app <see cref="IConfiguration"/> from environment variables</description></item>
/// <item><description>load app <see cref="IConfiguration"/> from supplied command line args</description></item>
/// <item><description>configure the <see cref="ILoggerFactory"/> to log to the console, debug, and event source output</description></item>
/// <item><description>enables scope validation on the dependency injection container when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development'</description></item>
/// </list>
/// </remarks>
/// <param name="args">The command line args.</param>
/// <returns>The initialized <see cref="IHostBuilder"/>.</returns>
public static IHostBuilder CreateDefaultBuilder(string[] args)
{
var builder = new HostBuilder();
builder.UseContentRoot(Directory.GetCurrentDirectory());
builder.ConfigureHostConfiguration(config =>
{
config.AddEnvironmentVariables(prefix: "DOTNET_");
if (args != null)
{
config.AddCommandLine(args);
}
});
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
IHostEnvironment env = hostingContext.HostingEnvironment;
bool reloadOnChange = hostingContext.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true);
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: reloadOnChange)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: reloadOnChange);
if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName))
{
var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
if (appAssembly != null)
{
config.AddUserSecrets(appAssembly, optional: true);
}
}
config.AddEnvironmentVariables();
if (args != null)
{
config.AddCommandLine(args);
}
})
.ConfigureLogging((hostingContext, logging) =>
{
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
// IMPORTANT: This needs to be added *before* configuration is loaded, this lets
// the defaults be overridden by the configuration.
if (isWindows)
{
// Default the EventLogLoggerProvider to warning or above
logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
}
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
logging.AddEventSourceLogger();
if (isWindows)
{
// Add the EventLogLoggerProvider on windows machines
logging.AddEventLog();
}
logging.Configure(options =>
{
options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId
| ActivityTrackingOptions.TraceId
| ActivityTrackingOptions.ParentId;
});
})
.UseDefaultServiceProvider((context, options) =>
{
bool isDevelopment = context.HostingEnvironment.IsDevelopment();
options.ValidateScopes = isDevelopment;
options.ValidateOnBuild = isDevelopment;
});
return builder;
}
然后是ConfigureWebHostDefaults
方法,它完成了以下事情,也提供了“原料”
- 使用默認值配置IHostBuilder來托管Web應用程序
- 使用Kestrel作為Web服務器,並使用應用程序的配置提供程序對其進行配置
- 配置
IWebHostEnvironment.WebRootFileProvider
以包含開發過程中入口程序集引用的項目中的Web靜態文件 - 加入
HostFiltering
中間件 - 當
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
時,加入ForwardedHeaders
中間件 - 啟用IIS集成
- 通過類型為
Action<IWebHostBuilder>
的configure
委托來進一步配置依賴注入服務和中間件(也是就是Startup
文件)
/// <summary>
/// Configures a <see cref="IHostBuilder" /> with defaults for hosting a web app.
/// </summary>
/// <remarks>
/// The following defaults are applied to the <see cref="IHostBuilder"/>:
/// <list type="bullet">
/// <item><description>use Kestrel as the web server and configure it using the application's configuration providers</description></item>
/// <item><description>configure <see cref="IWebHostEnvironment.WebRootFileProvider"/> to include static web assets from projects referenced by the entry assembly during development</description></item>
/// <item><description>adds the HostFiltering middleware</description></item>
/// <item><description>adds the ForwardedHeaders middleware if ASPNETCORE_FORWARDEDHEADERS_ENABLED=true,</description></item>
/// <item><description>enable IIS integration</description></item>
/// </list>
/// </remarks>
/// <param name="builder">The <see cref="IHostBuilder" /> instance to configure.</param>
/// <param name="configure">The configure callback</param>
/// <returns>A reference to the <paramref name="builder"/> after the operation has completed.</returns>
public static IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure)
{
if (configure is null)
{
throw new ArgumentNullException(nameof(configure));
}
return builder.ConfigureWebHost(webHostBuilder =>
{
WebHost.ConfigureWebDefaults(webHostBuilder);
configure(webHostBuilder);
});
}
internal static void ConfigureWebDefaults(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration((ctx, cb) =>
{
if (ctx.HostingEnvironment.IsDevelopment())
{
StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment, ctx.Configuration);
}
});
builder.UseKestrel((builderContext, options) =>
{
options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true);
})
.ConfigureServices((hostingContext, services) =>
{
// Fallback
services.PostConfigure<HostFilteringOptions>(options =>
{
if (options.AllowedHosts == null || options.AllowedHosts.Count == 0)
{
// "AllowedHosts": "localhost;127.0.0.1;[::1]"
var hosts = hostingContext.Configuration["AllowedHosts"]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
// Fall back to "*" to disable.
options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" });
}
});
// Change notification
services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>(
new ConfigurationChangeTokenSource<HostFilteringOptions>(hostingContext.Configuration));
services.AddTransient<IStartupFilter, HostFilteringStartupFilter>();
if (string.Equals("true", hostingContext.Configuration["ForwardedHeaders_Enabled"], StringComparison.OrdinalIgnoreCase))
{
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
// Only loopback proxies are allowed by default. Clear that restriction because forwarders are
// being enabled by explicit configuration.
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
services.AddTransient<IStartupFilter, ForwardedHeadersStartupFilter>();
}
services.AddRouting();
})
.UseIIS()
.UseIISIntegration();
}
最后來看Build
方法,它完成了以下事情,負責使用以上提供的原料來“建造”Host
- 保證
Build
方法只能被調用一次 - 使用以上提供的各種
IConfigurationBuilder
構建Host
配置 - 創建托管環境
- 創建
Host Builder
上下文 - 使用以上提供的各種
IConfigurationBuilder
構建應用程序配置 - 創建
Service Provider
(服務提供者),也就是我們常說的依賴注入容器 - 從容器中獲取
IHost
服務並返回
/// <summary>
/// Run the given actions to initialize the host. This can only be called once.
/// </summary>
/// <returns>An initialized <see cref="IHost"/></returns>
public IHost Build()
{
if (_hostBuilt)
{
throw new InvalidOperationException("Build can only be called once.");
}
_hostBuilt = true;
BuildHostConfiguration();
CreateHostingEnvironment();
CreateHostBuilderContext();
BuildAppConfiguration();
CreateServiceProvider();
return _appServices.GetRequiredService<IHost>();
}
這里的CreateServiceProvider
比較有意思,我把代碼貼下面供大家閱讀
private void CreateServiceProvider()
{
var services = new ServiceCollection();
#pragma warning disable CS0618 // Type or member is obsolete
services.AddSingleton<IHostingEnvironment>(_hostingEnvironment);
#pragma warning restore CS0618 // Type or member is obsolete
services.AddSingleton<IHostEnvironment>(_hostingEnvironment);
services.AddSingleton(_hostBuilderContext);
// register configuration as factory to make it dispose with the service provider
services.AddSingleton(_ => _appConfiguration);
#pragma warning disable CS0618 // Type or member is obsolete
services.AddSingleton<IApplicationLifetime>(s => (IApplicationLifetime)s.GetService<IHostApplicationLifetime>());
#pragma warning restore CS0618 // Type or member is obsolete
services.AddSingleton<IHostApplicationLifetime, ApplicationLifetime>();
services.AddSingleton<IHostLifetime, ConsoleLifetime>();
services.AddSingleton<IHost, Internal.Host>();
services.AddOptions();
services.AddLogging();
foreach (Action<HostBuilderContext, IServiceCollection> configureServicesAction in _configureServicesActions)
{
configureServicesAction(_hostBuilderContext, services);
}
object containerBuilder = _serviceProviderFactory.CreateBuilder(services);
foreach (IConfigureContainerAdapter containerAction in _configureContainerActions)
{
containerAction.ConfigureContainer(_hostBuilderContext, containerBuilder);
}
_appServices = _serviceProviderFactory.CreateServiceProvider(containerBuilder);
if (_appServices == null)
{
throw new InvalidOperationException($"The IServiceProviderFactory returned a null IServiceProvider.");
}
// resolve configuration explicitly once to mark it as resolved within the
// service provider, ensuring it will be properly disposed with the provider
_ = _appServices.GetService<IConfiguration>();
}
最后的最后,在Main
調用了IHost
的Run
方法,使用“建造”好的Host
來跑應用程序
總結
以上內容,源碼才是精華,我只是一個無情的翻譯機器,順便把大體流程梳理了一下。個人認為,讀懂源碼需要掌握但不限於以下知識(只列舉在ASP.NET Core中大量使用的)
- 委托
- 函數式編程
- DI、IOC概念,services生命周期
- 建造者模式
- 工廠模式
- Provider概念
- 擴展方法
- 反射
除了默認的實現,我們還可以通過一系列IHostBuilder
的方法、擴展方法來自定義我們應用程序的初始化過程
了解了應用程序的啟動過程,下一篇介紹依賴注入容器相關的內容