歡迎光臨
每天分享高質量文章

深入原始碼理解.NET Core中Startup的註冊及執行

開發.NET Core應用,直接映入眼簾的就是Startup類和Program類,它們是.NET Core應用程式的起點。透過使用Startup,可以配置化處理所有嚮應用程式所做的請求的管道,同時也可以減少.NET應用程式對單一伺服器的依賴性,使我們在更大程度上專註於面向多伺服器為中心的開發樣式。

目錄:

  • Startup討論
    • Starup所承擔的角色
    • Startup編寫規範
    • ConfigureServices
    • Configure
    • 擴充套件Startup方法
  • 深入原始碼檢視Startup是如何註冊和執行的
    • UseStartup原始碼
    • 建立Startup實體
    • ConfigureServices和Configure

Starup所承擔的角色

Startup類是ASP.NET Core程式中所必須的,可以使用多種修飾符(public、protect,private、internal),作為ASP.NET Core應用程式的入口,它包含與應用程式相關配置的功能或者說是介面。

雖然在程式裡我們使用的類名就是Startup,但是需要註意的是,Startup是一個抽象概念,你完全可以名稱成其他的,比如MyAppStartup或者其他的什麼名稱,只要你在Program類中啟動你所定義的啟動類即可。

以下是基於ASP.NET Core Preview 3模板中提供的寫法:

   1:  public class Program
   2:  {
   3:      public static void Main(string[] args)
   4:      {
   5:          CreateHostBuilder(args).Build().Run();
   6:      }
   7:   
   8:      public static IHostBuilder CreateHostBuilder(string[] args) =>
   9:          Host.CreateDefaultBuilder(args)
  10:              .ConfigureWebHostDefaults(webBuilder =>
  11:              {
  12:                  webBuilder.UseStartup();
  13:              });
  14:  }

不管你命名成什麼,只要將webBuilder.UseStartup<>()中的泛型類配置成你定義的入口類即可;

Startup編寫規範

下麵是ASP.NET Core 3.0 Preview 3模板中Startup的寫法:

   1:  // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
   2:  public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
   3:  {
   4:      if (env.IsDevelopment())
   5:      {
   6:          app.UseDeveloperExceptionPage();
   7:      }
   8:      else
   9:      {
  10:          // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
  11:          app.UseHsts();
  12:      }
  13:   
  14:      app.UseHttpsRedirection();
  15:   
  16:      app.UseRouting(routes =>
  17:      {
  18:          routes.MapControllers();
  19:      });
  20:   
  21:      app.UseAuthorization();
  22:  }

透過以上程式碼可以知道,Startup類中一般包括

  • 建構式:透過我們以前的開發經驗,我們可以知道,該構造方法可以包括多個物件
    • IConfiguration:表示一組鍵/值應用程式配置屬性。
    • IApplicationBuilder:是一個包含與當前環境相關的屬性和方法的介面。它用於獲取應用程式中的環境變數。
    • IHostingEnvironment:是一個包含與執行應用程式的Web宿主環境相關資訊的介面。使用這個介面方法,我們可以改變應用程式的行為。
    • ILoggerFactory:是為ASP.NET Core中的日誌記錄系統提供配置的介面。它還建立日誌系統的實體。
  • ConfigureServices
  • Configure

Startup在建立服務時,會執行依賴項註冊服務,以便在應用程式的其它地方使用這些依賴項。ConfigureServices 用於註冊服務,Configure 方法允許我們向HTTP管道新增中介軟體和服務。這就是ConfigureServices先於Configure 之前呼叫的原因。

ConfigureServices

該方法時可選的,非強制約束,它主要用於對依賴註入或ApplicationServices在整個應用中的支援,該方法必須是public的,其典型樣式是呼叫所有 Add{Service} 方法,主要場景包括物體框架、認證和 MVC 註冊服務:

   1:  services.AddDbContext(options =>options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
   2:  services.AddDefaultIdentity().AddDefaultUI(UIFramework.Bootstrap4).AddEntityFrameworkStores();
   3:  services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
   4:  // Add application services.此處主要是註冊IOC服務
   5:  services.AddTransient();
   6:  services.AddTransient();

Configure

該方法主要用於定義應用程式對每個HTTP請求的響應方式,即我們可以控制ASP.NET管道,還可用於在HTTP管道中配置中介軟體。請求管道中的每個中介軟體元件負責呼叫管道中的下一個元件,或在適當情況下使鏈發生短路。 如果中介軟體鏈中未發生短路,則每個中介軟體都有第二次機會在將請求傳送到客戶端前處理該請求。

該方法接受IApplicationBuilder作為引數,同時還可以接收其他一些可選引數,如IHostingEnvironment和ILoggerFactory。

一般而言,只要將服務註冊到configureServices方法中時,都可以在該方法中使用。

   1:  app.UseDeveloperExceptionPage();
   2:  app.UseHsts();
   3:  app.UseHttpsRedirection();
   4:  app.UseRouting(routes =>
   5:  {
   6:      routes.MapControllers();
   7:  });
   8:  app.UseAuthorization();

擴充套件Startup方法

使用IStartupFilter來對Startup功能進行擴充套件,在應用的Configure中介軟體管道的開頭或末尾使用IStartupFilter來配置中介軟體。IStartupFilter有助於確保當庫在應用請求處理管道的開端或末尾新增中介軟體的前後執行中介軟體。

以下是IStartupFilter的原始碼,透過原始碼我們可以知道,該介面有一個Action型別,並命名為Configure的方法。由於傳入引數型別和傳回型別一樣,這就保證了擴充套件的傳遞性及順序性,具體的演示程式碼,可以引數MSDN

   1:  using System;
   2:  using Microsoft.AspNetCore.Builder;
   3:
   4:  namespace Microsoft.AspNetCore.Hosting
   5:  {
   6:    public interface IStartupFilter
   7:    {
   8:        Action Configure(Action next);
   9:    }
  10:  }

此段文字,只是我想深入瞭解其內部機制而寫的,如果本身也不瞭解,其實是不影響我們正常編寫.NET Core應用的。

UseStartup原始碼

ASP.NET Core透過呼叫IWebHostBuilder.UseStartup方法,傳入Startup型別,註意開篇就已經說過Startup是一個抽象概念,我們看下原始碼:

   1:  ///
   2:   /// Specify the startup type to be used by the web host.
   3:   /// 
   4:   ///The  to configure.
   5:   ///The  to be used.
   6:   /// The .
   7:   public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType)
   8:   {
   9:       var startupAssemblyName = startupType.GetTypeInfo().Assembly.GetName().Name;
  10:
  11:      hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName);
  12:
  13:      // Light up the GenericWebHostBuilder implementation
  14:      if (hostBuilder is ISupportsStartup supportsStartup)
  15:      {
  16:          return supportsStartup.UseStartup(startupType);
  17:      }
  18:
  19:      return hostBuilder
  20:          .ConfigureServices(services =>
  21:          {
  22:              if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo()))
  23:              {
  24:                  services.AddSingleton(typeof(IStartup), startupType);
  25:              }
  26:              else
  27:              {
  28:                  services.AddSingleton(typeof(IStartup), sp =>
  29:                  {
  30:                      var hostingEnvironment = sp.GetRequiredService();
  31:                      return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName));
  32:                  });
  33:              }
  34:          });
  35:  }
  36:
  37:  ///
  38:  /// Specify the startup type to be used by the web host.
  39:  /// 
  40:  ///The  to configure.
  41:  /// The type containing the startup methods for the application.
  42:  /// The .
  43:  public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder) where TStartup :class
  44:  {
  45:      return hostBuilder.UseStartup(typeof(TStartup));
  46:  }

建立Startup實體

   1:  ///
   2:  /// Adds a delegate for configuring additional services for the host or web application. This may be called
   3:  /// multiple times.
   4:  /// 
   5:  ///A delegate for configuring the .
   6:  /// The .
   7:  public IWebHostBuilder ConfigureServices(Action configureServices)
   8:  {
   9:      if (configureServices == null)
  10:      {
  11:          throw new ArgumentNullException(nameof(configureServices));
  12:      }
  13:   
  14:      return ConfigureServices((_, services) => configureServices(services));
  15:  }
  16:   
  17:  ///
  18:  /// Adds a delegate for configuring additional services for the host or web application. This may be called
  19:  /// multiple times.
  20:  /// 
  21:  ///A delegate for configuring the .
  22:  /// The .
  23:  public IWebHostBuilder ConfigureServices(Action configureServices)
  24:  {
  25:      _configureServices += configureServices;
  26:      return this;
  27:  }

關於ConfigureServices的定義及註冊方式,是在IWebHostBuilder.ConfigureServices實現的,同時可以註意一下25行程式碼,向大家說明瞭多次註冊Startup的ConfigureServices方法時,會合併起來的根源。此處抽象委託用的也非常多。

該類裡面還有Build方法,我就不貼出程式碼了,只需要知道,主行程在此處開始了。接下來一個比較重要的方法,是BuildCommonServices,它向當前ServiceCollection中新增一些公共框架級服務,以下是部分程式碼,具體程式碼請檢視WebHostBuilder

   1:  try
   2:  {
   3:      var startupType = StartupLoader.FindStartupType(_options.StartupAssembly, _hostingEnvironment.EnvironmentName);
   4:   
   5:      if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo()))
   6:      {
   7:          services.AddSingleton(typeof(IStartup), startupType);
   8:      }
   9:      else
  10:      {
  11:          services.AddSingleton(typeof(IStartup), sp =>
  12:          {
  13:              var hostingEnvironment = sp.GetRequiredService();
  14:              var methods = StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName);
  15:              return new ConventionBasedStartup(methods);
  16:          });
  17:      }
  18:  }
  19:  catch (Exception ex)
  20:  {
  21:      var capture = ExceptionDispatchInfo.Capture(ex);
  22:      services.AddSingleton(_ =>
  23:      {
  24:          capture.Throw();
  25:          return null;
  26:      });
  27:  }
由此可見,如果我們的Startup類直接實現IStartup,它可以並且將直接註冊為IStartup的實現型別。只不過ASP.NET Core模板程式碼並沒有實現IStartup,它更多的是一種約定,並透過DI呼叫委託,依此呼叫Startup內的建構式還有另外兩個方法。
同時上述程式碼還展示瞭如何建立Startup型別,就是用到了靜態方法StartupLoader.LoadMethods類生成StartupMethods實體。

ConfigureServicesConfigure

當WebHost初始化時,框架會去查詢相應的方法,這裡,我們主要檢視原始碼,其中的核心方法是StartupLoader.FindMethods
   1:  private static MethodInfo FindMethod(Type startupType, string methodName, string environmentName, Type returnType = null, bool required = true)
   2:  {
   3:      var methodNameWithEnv = string.Format(CultureInfo.InvariantCulture, methodName, environmentName);
   4:      var methodNameWithNoEnv = string.Format(CultureInfo.InvariantCulture, methodName, "");
   5:   
   6:      var methods = startupType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
   7:      var selectedMethods = methods.Where(method => method.Name.Equals(methodNameWithEnv, StringComparison.OrdinalIgnoreCase)).ToList();
   8:      if (selectedMethods.Count > 1)
   9:      {
  10:          throw new InvalidOperationException(string.Format("Having multiple overloads of method '{0}' is not supported.", methodNameWithEnv));
  11:      }
  12:      if (selectedMethods.Count == 0)
  13:      {
  14:          selectedMethods = methods.Where(method => method.Name.Equals(methodNameWithNoEnv, StringComparison.OrdinalIgnoreCase)).ToList();
  15:          if (selectedMethods.Count > 1)
  16:          {
  17:              throw new InvalidOperationException(string.Format("Having multiple overloads of method '{0}' is not supported.", methodNameWithNoEnv));
  18:          }
  19:      }
  20:   
  21:      var methodInfo = selectedMethods.FirstOrDefault();
  22:      if (methodInfo == null)
  23:      {
  24:          if (required)
  25:          {
  26:              throw new InvalidOperationException(string.Format("A public method named '{0}' or '{1}' could not be found in the '{2}' type.",
  27:                  methodNameWithEnv,
  28:                  methodNameWithNoEnv,
  29:                  startupType.FullName));
  30:   
  31:          }
  32:          return null;
  33:      }
  34:      if (returnType != null && methodInfo.ReturnType != returnType)
  35:      {
  36:          if (required)
  37:          {
  38:              throw new InvalidOperationException(string.Format("The '{0}' method in the type '{1}' must have a return type of '{2}'.",
  39:                  methodInfo.Name,
  40:                  startupType.FullName,
  41:                  returnType.Name));
  42:          }
  43:          return null;
  44:      }
  45:      return methodInfo;
  46:  }
它查詢的第一個委託是ConfigureDelegate,該委託將用於構建應用程式的中介軟體管道。FindMethod完成了大部分工作,具體的程式碼請檢視StartupLoader此方法根據傳遞給它的methodName引數在Startup類中查詢響應的方法。
我們知道,Startup的定義更多的是約定,所以會去查詢Configure和ConfigureServices。當然,透過原始碼我還知道,除了提供標準的“Configure”方法之外,我們還可以透過環境配置找到響應的Configure和ConfigureServices。根本來說,我們最終查詢到的是ConfigureContainerDelegate。
接下來,一個比較重要的方法是LoadMethods
   1:  public static StartupMethods LoadMethods(IServiceProvider hostingServiceProvider, Type startupType, string environmentName)
   2:  {
   3:      var configureMethod = FindConfigureDelegate(startupType, environmentName);
   4:   
   5:      var servicesMethod = FindConfigureServicesDelegate(startupType, environmentName);
   6:      var configureContainerMethod = FindConfigureContainerDelegate(startupType, environmentName);
   7:   
   8:      object instance = null;
   9:      if (!configureMethod.MethodInfo.IsStatic || (servicesMethod != null && !servicesMethod.MethodInfo.IsStatic))
  10:      {
  11:          instance = ActivatorUtilities.GetServiceOrCreateInstance(hostingServiceProvider, startupType);
  12:      }
  13:   
  14:      // The type of the TContainerBuilder. If there is no ConfigureContainer method we can just use object as it's not
  15:      // going to be used for anything.
  16:      var type = configureContainerMethod.MethodInfo != null ? configureContainerMethod.GetContainerType() : typeof(object);
  17:   
  18:      var builder = (ConfigureServicesDelegateBuilder) Activator.CreateInstance(
  19:          typeof(ConfigureServicesDelegateBuilder<>).MakeGenericType(type),
  20:          hostingServiceProvider,
  21:          servicesMethod,
  22:          configureContainerMethod,
  23:          instance);
  24:   
  25:      return new StartupMethods(instance, configureMethod.Build(instance), builder.Build());
  26:  }
該方法透過查詢對應的方法,由於Startup並未在DI中註冊,所以會呼叫GetServiceOrCreateInstance建立一個Startup實體,此時建構式也在此得到解析。
透過一系列的呼叫,最終到達了ConfigureServicesBuilder.Invoke裡面。Invoke方法使用反射來獲取和檢查在Startup類上定義的ConfigureServices方法所需的引數。
   1:  private IServiceProvider InvokeCore(object instance, IServiceCollection services)
   2:  {
   3:      if (MethodInfo == null)
   4:      {
   5:          return null;
   6:      }
   7:   
   8:      // Only support IServiceCollection parameters
   9:      var parameters = MethodInfo.GetParameters();
  10:      if (parameters.Length > 1 ||
  11:          parameters.Any(p => p.ParameterType != typeof(IServiceCollection)))
  12:      {
  13:          throw new InvalidOperationException("The ConfigureServices method must either be parameterless or take only one parameter of type IServiceCollection.");
  14:      }
  15:   
  16:      var arguments = new object[MethodInfo.GetParameters().Length];
  17:   
  18:      if (parameters.Length > 0)
  19:      {
  20:          arguments[0] = services;
  21:      }
  22:   
  23:      return MethodInfo.Invoke(instance, arguments) as IServiceProvider;
  24:  }

最後我們來看一下ConfigureBuilder類,它需要一個Action委託變數,其中包含每個IStartupFilter的一組包裝的Configure方法,最後一個是Startup.Configure方法的委託。此時,所呼叫的配置鏈首先命中的是AutoRequestServicesStartupFilter.Configure方法。並將該委託鏈作為下一個操作,之後會呼叫ConventionBasedStartup.Configure方法。這將在其本地StartupMethods物件上呼叫ConfigureDelegate。

   1:  private void Invoke(object instance, IApplicationBuilder builder)
   2:  {
   3:      // Create a scope for Configure, this allows creating scoped dependencies
   4:      // without the hassle of manually creating a scope.
   5:      using (var scope = builder.ApplicationServices.CreateScope())
   6:      {
   7:          var serviceProvider = scope.ServiceProvider;
   8:          var parameterInfos = MethodInfo.GetParameters();
   9:          var parameters = new object[parameterInfos.Length];
  10:          for (var index = 0; index < parameterInfos.Length; index++)
  11:          {
  12:              var parameterInfo = parameterInfos[index];
  13:              if (parameterInfo.ParameterType == typeof(IApplicationBuilder))
  14:              {
  15:                  parameters[index] = builder;
  16:              }
  17:              else
  18:              {
  19:                  try
  20:                  {
  21:                      parameters[index] = serviceProvider.GetRequiredService(parameterInfo.ParameterType);
  22:                  }
  23:                  catch (Exception ex)
  24:                  {
  25:                      throw new Exception(string.Format(
  26:                          "Could not resolve a service of type '{0}' for the parameter '{1}' of method '{2}' on type '{3}'.",
  27:                          parameterInfo.ParameterType.FullName,
  28:                          parameterInfo.Name,
  29:                          MethodInfo.Name,
  30:                          MethodInfo.DeclaringType.FullName), ex);
  31:                  }
  32:              }
  33:          }
  34:          MethodInfo.Invoke(instance, parameters);
  35:      }
  36:  }

Startup.Configure方法會呼叫ServiceProvider所解析的相應的引數,該方法還可以使用IApplicationBuilder將中介軟體新增到應用程式管道中。最終的RequestDelegate是從IApplicationBuilder構建並傳回的,至此WebHost初始化完成。

已同步到看一看
贊(0)

分享創造快樂