ASP.NET Core[原始碼分析篇] - Startup

wu發表於2019-07-22

  應用啟動的重要類 - Startup

  在ASP.NET Core - 從Program和Startup開始這篇文章裡面,我們知道了Startup這個類的重要性,它主要負責了:

  1. 配置應用需要的服務(服務註冊,ConfigureServices方法)。
  2. 建立應用的請求處理處理管道(Configure方法)。  

  在原始碼分析之前補充一點,雖然我們一般是按約定把這個類名定義成了Startup,但是在真正應用中,我們不是必須要命名為Startup的,這只是一個抽象概念,我們可以命名其他的類名,只需要在UseStartup/UseStartup<TStartup>中顯式註冊這個啟動類即可,系統會把這個啟動類註冊為單例,例如:  

public class Program
{
    public static void Main(string[] args)
    {
        BuildWebHost(args).Run();
    }

    public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<YourStartupClass>()
            .Build();
}

public class YourStartupClass
{
    public void ConfigureService(IServiceCollection services)
    {
    }

    public void Configure(IApplicationBuilder app)  
    {
    }    
}

  Startup是如何被註冊進來的?

   從前面我們可以看到Startup是在UseStartup方法裡面被引用進來,我們先看一下UseStartup是如何把Startup類註冊進來的  

 /// <summary>Specify the startup type to be used by the web host.</summary>
    /// <param name="hostBuilder">The <see cref="T:Microsoft.AspNetCore.Hosting.IWebHostBuilder" /> to configure.</param>
    /// <param name="startupType">The <see cref="T:System.Type" /> to be used.</param>
    /// <returns>The <see cref="T:Microsoft.AspNetCore.Hosting.IWebHostBuilder" />.</returns>
    public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder,Type startupType)
    {
      string name = startupType.GetTypeInfo().Assembly.GetName().Name;
      return hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, name).ConfigureServices((Action<IServiceCollection>) (services =>
      {
        if (typeof (IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo()))
          ServiceCollectionServiceExtensions.AddSingleton(services, typeof (IStartup), startupType);
        else
          ServiceCollectionServiceExtensions.AddSingleton(services, typeof (IStartup), (Func<IServiceProvider, object>) (sp =>
          {
            IHostingEnvironment requiredService = sp.GetRequiredService<IHostingEnvironment>();
            return (object) new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, requiredService.EnvironmentName));
          }));
      }));
    }

    /// <summary>Specify the startup type to be used by the web host.</summary>
    /// <param name="hostBuilder">The <see cref="T:Microsoft.AspNetCore.Hosting.IWebHostBuilder" /> to configure.</param>
    /// <typeparam name="TStartup">The type containing the startup methods for the application.</typeparam>
    /// <returns>The <see cref="T:Microsoft.AspNetCore.Hosting.IWebHostBuilder" />.</returns>
    public static IWebHostBuilder UseStartup<TStartup>(this IWebHostBuilder hostBuilder)
      where TStartup : class
    {
      return hostBuilder.UseStartup(typeof (TStartup));
    }

  _configureServicesDelegates  

  從上面程式碼我們可以看出,這裡主要是呼叫了WebHostBuilder的ConfigureServices方法,我們看一下ConfigureServices做了什麼

public IWebHostBuilder ConfigureServices( Action<IServiceCollection> configureServices)
    {
      if (configureServices == null)
        throw new ArgumentNullException(nameof (configureServices));
      return this.ConfigureServices((Action<WebHostBuilderContext, IServiceCollection>) ((_, 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="T:Microsoft.Extensions.DependencyInjection.IServiceCollection" />.</param>
    /// <returns>The <see cref="T:Microsoft.AspNetCore.Hosting.IWebHostBuilder" />.</returns>
    public IWebHostBuilder ConfigureServices( Action<WebHostBuilderContext, IServiceCollection> configureServices)
    {
      if (configureServices == null)
        throw new ArgumentNullException(nameof (configureServices));
      this._configureServicesDelegates.Add(configureServices);
      return (IWebHostBuilder) this;
    }

  這裡主要是把委託新增到_configureServicesDelegates列表裡面,這個_configureServicesDelegates有什麼用呢?這個屬性是一個非常重要的承載角色,在後面的WebHost真正呼叫Build方法時,我們再詳細講解。  

  再次看回UseStartup,這裡呼叫了ConfigureServices並向_configureServicesDelegates註冊了一個委託,我們看一下這個委託的實體,這裡面有兩個分支:

  1. Startup實現IStartup介面

  直接註冊該Startup為單例。從這裡看出,其實我們的Startup類還有另一種方式實現的,就是直接實現IStartup介面。(其實還有一種是實現StartupBase)

  2. Startup沒實現IStartup介面

  註冊型別為ConventionBasedStartupStartup型別

public class ConventionBasedStartup : IStartup
  {
    private readonly StartupMethods _methods;

    public ConventionBasedStartup(StartupMethods methods)
    {
      this._methods = methods;
    }

    public void Configure(IApplicationBuilder app)
    {
      try
      {
        this._methods.ConfigureDelegate(app);
      }
      catch (Exception ex)
      {
        if (ex is TargetInvocationException)
          ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
        throw;
      }
    }

    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
      try
      {
        return this._methods.ConfigureServicesDelegate(services);
      }
      catch (Exception ex)
      {
        if (ex is TargetInvocationException)
          ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
        throw;
      }
    }
  }  

  注意這個ConventionBasedStartup其實也是實現了IStartup介面,ConventionBasedStartup物件是根據一個StartupMethods物件建立的,我們來看一下這個StartupMethods型別的定義

public class StartupMethods
  {
    public StartupMethods(object instance,Action<IApplicationBuilder> configure, Func<IServiceCollection, IServiceProvider> configureServices)
    {
      this.StartupInstance = instance;
      this.ConfigureDelegate = configure;
      this.ConfigureServicesDelegate = configureServices;
    }

    public object StartupInstance { get; }

    public Func<IServiceCollection, IServiceProvider> ConfigureServicesDelegate { get; }

    public Action<IApplicationBuilder> ConfigureDelegate { get; }
  }

  StartupMethods只提供兩個註冊服務和中介軟體的方法,這兩個方法體現在由它的兩個屬性(ConfigureServicesDelegate和ConfigureDelegate)提供的兩個委託物件。

  在我們UseStartup程式碼裡面,是通過StartupLoader.LoadMethods基於Startup型別獲取到一個StartupMethods

public class StartupLoader
  {
    public static StartupMethods LoadMethods(IServiceProvider hostingServiceProvider,Type startupType,string environmentName)
    {
      ConfigureBuilder configureDelegate = StartupLoader.FindConfigureDelegate(startupType, environmentName);
      ConfigureServicesBuilder servicesDelegate = StartupLoader.FindConfigureServicesDelegate(startupType, environmentName);
      ConfigureContainerBuilder containerDelegate = StartupLoader.FindConfigureContainerDelegate(startupType, environmentName);
      object instance1 = (object) null;
      if (!configureDelegate.MethodInfo.IsStatic || servicesDelegate != null && !servicesDelegate.MethodInfo.IsStatic)
        instance1 = ActivatorUtilities.GetServiceOrCreateInstance(hostingServiceProvider, startupType);
      StartupLoader.ConfigureServicesDelegateBuilder instance2 = (StartupLoader.ConfigureServicesDelegateBuilder) Activator.CreateInstance(typeof (StartupLoader.ConfigureServicesDelegateBuilder<>).MakeGenericType(containerDelegate.MethodInfo != (MethodInfo) null ? containerDelegate.GetContainerType() : typeof (object)), (object) hostingServiceProvider, (object) servicesDelegate, (object) containerDelegate, instance1);
      return new StartupMethods(instance1, configureDelegate.Build(instance1), instance2.Build());
    }

    private static ConfigureBuilder FindConfigureDelegate(Type startupType, string environmentName)
    {
      return new ConfigureBuilder(StartupLoader.FindMethod(startupType, "Configure{0}", environmentName, typeof (void), true));
    }

    private static ConfigureContainerBuilder FindConfigureContainerDelegate(Type startupType, string environmentName)
    {
      return new ConfigureContainerBuilder(StartupLoader.FindMethod(startupType, "Configure{0}Container", environmentName, typeof (void), false));
    }

    private static ConfigureServicesBuilder FindConfigureServicesDelegate( Type startupType,string environmentName)
    {
      MethodInfo method = StartupLoader.FindMethod(startupType, "Configure{0}Services", environmentName, typeof (IServiceProvider), false);
      if ((object) method == null)
        method = StartupLoader.FindMethod(startupType, "Configure{0}Services", environmentName, typeof (void), false);
      return new ConfigureServicesBuilder(method);
    }

    private static MethodInfo FindMethod( Type startupType,string methodName,string environmentName,Type returnType = null, bool required = true)
    {
      string methodNameWithEnv = string.Format((IFormatProvider) CultureInfo.InvariantCulture, methodName, (object) environmentName);
      string methodNameWithNoEnv = string.Format((IFormatProvider) CultureInfo.InvariantCulture, methodName, (object) "");
      MethodInfo[] methods = startupType.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public);
      List<MethodInfo> list = ((IEnumerable<MethodInfo>) methods).Where<MethodInfo>((Func<MethodInfo, bool>) (method => method.Name.Equals(methodNameWithEnv, StringComparison.OrdinalIgnoreCase))).ToList<MethodInfo>();
      if (list.Count > 1)
        throw new InvalidOperationException(string.Format("Having multiple overloads of method '{0}' is not supported.", (object) methodNameWithEnv));
      if (list.Count == 0)
      {
        list = ((IEnumerable<MethodInfo>) methods).Where<MethodInfo>((Func<MethodInfo, bool>) (method => method.Name.Equals(methodNameWithNoEnv, StringComparison.OrdinalIgnoreCase))).ToList<MethodInfo>();
        if (list.Count > 1)
          throw new InvalidOperationException(string.Format("Having multiple overloads of method '{0}' is not supported.", (object) methodNameWithNoEnv));
      }
      MethodInfo methodInfo = list.FirstOrDefault<MethodInfo>();
      if (methodInfo == (MethodInfo) null)
      {
        if (required)
          throw new InvalidOperationException(string.Format("A public method named '{0}' or '{1}' could not be found in the '{2}' type.", (object) methodNameWithEnv, (object) methodNameWithNoEnv, (object) startupType.FullName));
        return (MethodInfo) null;
      }
      if (!(returnType != (Type) null) || !(methodInfo.ReturnType != returnType))
        return methodInfo;
      if (required)
        throw new InvalidOperationException(string.Format("The '{0}' method in the type '{1}' must have a return type of '{2}'.", (object) methodInfo.Name, (object) startupType.FullName, (object) returnType.Name));
      return (MethodInfo) null;
    }
    
}

   這裡面主要是通過反射獲取startupType裡面的Configure和ConfigureServices作為引數賦值給StartupMethods的ConfigureDelegate和ConfigureServicesDelegate,這樣一個完整的ConventionBasedStartup型別的startup就被註冊為單例了。

  優先順序

  比較有意思的是,我們可以看到,Startup類中的這兩個方法除了可以命名為ConfigureServices和Configure之外,它們還可以攜帶執行環境名稱,具體採用的格式分別為Configure{EnvironmentName}Services和Configure{EnvironmentName},後者具有更高的選擇優先順序。

  注意到FindConfigureServicesDelegate這個方法的實現,一般來說,ConfigureServices/Configure{EnvironmentName}Services這個方法不具有返回值(返回型別為void),但是它也可以定義成一個返回型別為IServiceProvider的方法。如果這個方法返回一個ServiceProvider物件,後續過程中獲取的所有服務將從這個ServiceProvider中提取。這個返回的ServiceProvider對我們後續的一個註冊是非常有用的,具體在【ASP.NET Core - 利用Windsor Castle實現通用註冊】這篇文章中提到,我們基於返回的IServiceProvider進行容器替換進而實現通用註冊,對於沒有返回值的情況,系統會根據當前註冊的服務建立一個ServiceProvider。

  注意到此為止,程式還是沒有執行做一個真正的註冊,因為我們只是往_configureServicesDelegates新增了委託而已,並沒有執行,這個的執行是在WebHost真正呼叫Build方法時。

  其實Create­DefaultBuilder方法中的其他幾個UseXXX(UserUrl,UseKestrel等)擴充套件方法也是同樣的道理,把對應需要註冊的Action委託同樣寫入了configureServicesDelegates

  總結

  上面的這個過程,我們看到其實包含很多隱含的邏輯,這些邏輯的理解,可以提供多種選項讓我們在後面的真正開發中能夠根據自己專案的一個實際情況進行選擇,到此為止,我們現在能看到的最重要的一個點是:_configureServicesDelegates的一個承載作用。

相關文章