ASP.NET Core - 源碼解析 - Program.cs (一)

建立一個 asp.net core的項目

  1. 參考微軟文檔吧, 很詳細: https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/start-mvc?view=aspnetcore-2.2&tabs=visual-studioios

  2. 建立完後會有個web項目, 打開後我這面的層級目錄以下:git

代碼解析

//爲何關注這個類, 由於這裏有main函數, 通常來講main函數都是程序啓動的時候的啓動類. 看一下這行代碼:
public static void Main(string[] args)
{
    CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>();

看到這個 Main 函數時我頓時有種似曾相識的感受, 這個不是 console application 中的 Main 函數麼, 兩者莫非有關聯?github

在個人理解, 其實asp.net core web application其本質應該就是一個 console application,而後經過內置的 web server 去處理 http 請求。這個內置的 web sever 默認是 Kestrel,(想了解 Kestrel 的話能夠移步這裏) , 可是 Web server 通常是須要 host在某個宿主上的, 比較 common 的 host 環境有 IIS, windows servie, Nginx, 或者 Docker等等. 這時候你要問我宿主是幹什麼用的以及爲什麼要host到宿主上面? 我以爲這就稍微超出咱們這篇文章的 scope 了. 簡單來講就是宿主是用來啓動咱們的 asp.net core 的程序的. 而且會監聽程序的運行狀態可以作到一些配置和審覈權限等其餘的工做, 而且咱們的 web server 能夠作成分佈式的 server. 它們都分別接收由宿主分發過來的 http 請求, 那麼宿主又能夠承擔做爲一個反向代理的角色. 對這些內容若是感興趣的話能夠看看官方的材料, https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?tabs=aspnetcore2x&view=aspnetcore-2.2, 這裏就很少寫了.web

那麼回頭來看 asp.net core 的 main 函數都作了什麼?json

  1. 建立 WebHostBuilder 對象
  2. 讓這個 WebHostBuilder 對象 build一個 webhost 並run起來

建立 WebHostBuilder 對象

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
    .UseStartup<Startup>();

分開來看, 先看第一部分 WebHost.CreateDefaultBuilder(args)

打開源碼文件路徑(..\AspNetCore-2.2.4\src\DefaultBuilder\src)(由於源碼中不少叫WebHost的文件, 容易找不到), 咱們來看一下 CreateDefaultBuilder 這個方法的源碼:windows

/// <summary>
/// Initializes a new instance of the <see cref="WebHostBuilder"/> class with pre-configured defaults.
/// </summary>
/// <remarks>
///   The following defaults are applied to the returned <see cref="WebHostBuilder"/>:
///     use Kestrel as the web server and configure it using the application's configuration providers,
///     set the <see cref="IHostingEnvironment.ContentRootPath"/> to the result of <see cref="Directory.GetCurrentDirectory()"/>,
///     load <see cref="IConfiguration"/> from 'appsettings.json' and 'appsettings.[<see cref="IHostingEnvironment.EnvironmentName"/>].json',
///     load <see cref="IConfiguration"/> from User Secrets when <see cref="IHostingEnvironment.EnvironmentName"/> is 'Development' using the entry assembly,
///     load <see cref="IConfiguration"/> from environment variables,
///     load <see cref="IConfiguration"/> from supplied command line args,
///     configure the <see cref="ILoggerFactory"/> to log to the console and debug output,
///     and enable IIS integration.
/// </remarks>
/// <param name="args">The command line args.</param>
/// <returns>The initialized <see cref="IWebHostBuilder"/>.</returns>
public static IWebHostBuilder CreateDefaultBuilder(string[] args)
{
    var builder = new WebHostBuilder();

    if (string.IsNullOrEmpty(builder.GetSetting(WebHostDefaults.ContentRootKey)))
    {
        builder.UseContentRoot(Directory.GetCurrentDirectory());
    }
    if (args != null)
    {
        builder.UseConfiguration(new ConfigurationBuilder().AddCommandLine(args).Build());
    }

    builder.UseKestrel((builderContext, options) =>
        {
            options.Configure(builderContext.Configuration.GetSection("Kestrel"));
        })
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            var env = hostingContext.HostingEnvironment;

            config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                  .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

            if (env.IsDevelopment())
            {
                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) =>
        {
            logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
            logging.AddConsole();
            logging.AddDebug();
            logging.AddEventSourceLogger();
        })
        .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>();
        })
        .UseIIS()
        .UseIISIntegration()
        .UseDefaultServiceProvider((context, options) =>
        {
            options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
        });

    return builder;
}

根據源碼,咱們來總結一下 CreateDefaultBuilder 所作的工做:緩存

UseKestrel:使用Kestrel做爲Web server。 UseContentRoot:指定Web host使用的content root(內容根目錄),好比Views。默認爲當前應用程序根目錄。 ConfigureAppConfiguration:設置當前應用程序配置。主要是讀取 appsettinggs.json 配置文件、開發環境中配置的UserSecrets、添加環境變量和命令行參數 。 ConfigureLogging:讀取配置文件中的Logging節點,配置日誌系統。 UseIISIntegration:使用IISIntegration 中間件。 UseDefaultServiceProvider:設置默認的依賴注入容器。cookie

再看第二部分 .UseStartup<Startup>();

咱們直接F12mvc

//
// Summary:
//     Specify the startup type to be used by the web host.
//
// Parameters:
//   hostBuilder:
//     The Microsoft.AspNetCore.Hosting.IWebHostBuilder to configure.
//
// Type parameters:
//   TStartup:
//     The type containing the startup methods for the application.
//
// Returns:
//     The Microsoft.AspNetCore.Hosting.IWebHostBuilder.
public static IWebHostBuilder UseStartup<TStartup>(this IWebHostBuilder hostBuilder) where TStartup : class;

固然這些不夠, 咱們的目的是知道底層是用怎麼使用的startupapp

/// <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;

    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));
                });
            }
        });
}

能夠看到其實UseStartup方法返回的是IWebHostBuilder對象, 其中.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName) 是將 ApplicationKeystartupAssemblyName 緩存起來. 以下圖:

而後.ConfigureServices(Action<IServiceCollection> configureServices) 實際上也是對list集合進行一個緩存操做, 可是注意這個方法的參數是一個委託Action, 實際上調用的時候傳遞的是一個lambda表達式, 咱們須要看看這個表達式裏的神奇操做:(咱們主要關注 else 裏面的部分, if中的沒啥可說的.)

services.AddSingleton(typeof(IStartup), sp =>
{
    var hostingEnvironment = sp.GetRequiredService<IHostingEnvironment>();
    return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName));
});

services是咱們 asp.net core DI的核心ServiceCollection , 而後AddSingleton方法是它內部的一個靜態方法

public static IServiceCollection AddSingleton(this IServiceCollection services, Type serviceType, Func<IServiceProvider, object> implementationFactory);

因此咱們看到的 sp 其實是 IServiceProvider, 先暫時理解成是用來搞到 service 的.
而後精髓來了,

StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName)

參數咱們差很少都知道表明什麼. 可是這個LoadMethods 是幹嗎的?? 咱們看一下解釋:

很長, 可是主要就是用於經過反射技術, 將咱們自定義的StartUp.cs文件裏面的方法都load到低層來. 而後順便配置一下每次都有的那個service和建立一個request pipeline of the application

這裏我以爲有必要將startup.cs文件中的兩個function貼到下面方便咱們後續的擼源碼:

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        // This lambda determines whether user consent for non-essential cookies is needed for a given request.
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });


    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        // 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.UseStaticFiles();
    app.UseCookiePolicy();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

看完這兩個方法, 咱們都能明白一個事兒就是這兩個方法都是運行時調用的. 至於爲何是運行時調用的, 我們差很少都知道了, 由於底層build的時候反射獲取這兩個方法, 將兩個方法的配置參數什麼的配置進底層的service中.

這裏有必要解釋一下咱們的自定義 startup 文件裏面的參數含義, 由於這畢竟是暴露給咱們開發者使用的, 因此應該多瞭解一下: IServiceCollection:當前容器中各服務的配置集合,ASP.NET Core內置的依賴注入容器。 IApplicationBuilder:用於構建應用程序的請求管道。 IHostingEnvironment:提供了當前的 EnvironmentName、WebRootPath 以及 ContentRoot等。

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());
}

(反射已經用到了出神入化的地步) 想看怎麼實現的就本身去看吧. 反正代碼貼到這裏, 意思明白了就行.

好了, 目前知道了.ConfigureService 的參數是幹嗎的了... 那看看這個方法的底層要這個參數作啥了吧:

private readonly List<Action<WebHostBuilderContext, IServiceCollection>> _configureServicesDelegates;

/// <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)
{
    if (configureServices == null)
            {
                throw new ArgumentNullException(nameof(configureServices));
            }

    _configureServicesDelegates.Add(configureServices);
    return this;
}

臥槽槽, 又是一個緩存....行吧, 底層各類搞緩存, 不就是DI的本質嗎..這算基本搞懂了UseStartup..

建立 WebHostBuilder 對象完事了

那麼WebHostBuilder建立出來後, 裏面有兩個緩存, 對startup的類型進行緩存以外, 還對startup的services進行緩存. 而後這個builder顯然是要進行 build 的, 否則費那麼大力氣建立幹嗎. 因此看下一篇吧, 解讀後續操做.

相關文章
相關標籤/搜索