重新整理 .net core 實踐篇————配置應用[一]

不問前世發表於2021-05-23

前言

本來想整理到<<重新整理.net core 計1400篇>>裡面去,但是後來一想,整理 .net core 實踐篇 是偏於實踐,故而分開。

因為是重新整理,那麼就從配置開始整理。以下只是個人理解,如有錯誤,望請指點謝謝。

正文

在我們建立好一個應用的時候,那麼出現在我們視野的是一個這樣的東西:

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

	public static IHostBuilder CreateHostBuilder(string[] args) =>
		Host.CreateDefaultBuilder(args)
			.ConfigureWebHostDefaults(webBuilder =>
			{
				webBuilder.UseStartup<Startup>();
			});
}

看到這種:

CreateHostBuilder(args).Build().Run();

顯然是建設者模式。

那麼前面的基本就是在做一個構建。

既然是一個建設者模式,那麼就來看一下這個的構建器是什麼?

是一個叫做IHOSTBUILDER 的東西哈,而顯然從命名上看這是一個介面哈。那麼看下這個介面是啥:

跟配置相關得到也就是那幾個Configure開頭的那4個東西。

後面兩個UseServiceProviderFactory後面系列再說。

//
// 摘要:
//     Sets up the configuration for the remainder of the build process and application.
//     This can be called multiple times and the results will be additive. The results
//     will be available at Microsoft.Extensions.Hosting.HostBuilderContext.Configuration
//     for subsequent operations, as well as in Microsoft.Extensions.Hosting.IHost.Services.
//
// 引數:
//   configureDelegate:
//     The delegate for configuring the Microsoft.Extensions.Configuration.IConfigurationBuilder
//     that will be used to construct the Microsoft.Extensions.Configuration.IConfiguration
//     for the application.
//
// 返回結果:
//     The same instance of the Microsoft.Extensions.Hosting.IHostBuilder for chaining.
IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate);

這個函式的大致意思是說構件一個IConfigurationBuilder,這個the remainder of the build process and application.的配置。

// 摘要:
//     Enables configuring the instantiated dependency container. This can be called
//     multiple times and the results will be additive.
//
// 引數:
//   configureDelegate:
//     The delegate which configures the builder.
//
// 型別引數:
//   TContainerBuilder:
//     The type of builder.
//
// 返回結果:
//     The same instance of the Microsoft.Extensions.Hosting.IHostBuilder for chaining.
IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate);

依賴容器相關的哈。

ConfigureHostConfiguration 和 ConfigureServices 這兩個就不貼了,分別是一些壞境配置和服務配置。

那麼啟動一下,看下他們的執行順序是否和我們程式碼的書寫順序是否相同,也就是說不管我如何調換順序他們的執行都是按照某種規則。

那麼這裡加上log,通過log的方式來檢視執行順序,如下:

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

	public static IHostBuilder CreateHostBuilder(string[] args) =>
		Host.CreateDefaultBuilder(args)
			.ConfigureAppConfiguration((builder) =>
			{
				Console.WriteLine("ConfigureAppConfiguration");
			}).ConfigureServices(builder =>
			{
				Console.WriteLine("ConfigureServices");
			}).ConfigureHostConfiguration(builder =>
			{
				Console.WriteLine("ConfigureHostConfiguration");
			})
			.ConfigureWebHostDefaults(webBuilder =>
			{
				Console.WriteLine("ConfigureWebHostDefaults");
				webBuilder.UseStartup<Startup>();
			});
}

startup.cs

public class Startup
{
	public IConfiguration Configuration { get; }

	public Startup(IConfiguration configuration)
	{
		Console.WriteLine("Startup");
		Configuration = configuration;
	}

	// This method gets called by the runtime. Use this method to add services to the container.
	// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
	public void ConfigureServices(IServiceCollection services)
	{
		Console.WriteLine("Startup.ConfigureServices");
		services.AddControllers();
	}

	// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
	public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
	{
		Console.WriteLine("Startup.Configure");
		if (env.IsDevelopment())
		{
			app.UseDeveloperExceptionPage();
		}

		app.UseRouting();

		app.UseEndpoints(endpoints =>
		{
			endpoints.MapGet("/", async context =>
			{
				await context.Response.WriteAsync("Hello World!");
			});
		});
	}
}

這裡可以發現執行順序和我們的程式碼的書寫順序並不是一致的。

那麼我們再次做一個小小的調換,那麼會怎麼樣呢?

public static IHostBuilder CreateHostBuilder(string[] args) =>
	Host.CreateDefaultBuilder(args)
		.ConfigureWebHostDefaults(webBuilder =>
		{
			Console.WriteLine("ConfigureWebHostDefaults");
			webBuilder.UseStartup<Startup>();
		})
		.ConfigureServices(builder =>
		{
			Console.WriteLine("ConfigureServices");
		})
		.ConfigureAppConfiguration((builder) =>
		{
			Console.WriteLine("ConfigureAppConfiguration");
		})
		.ConfigureHostConfiguration(builder =>
		{
			Console.WriteLine("ConfigureHostConfiguration");
		});
}

發現變化的只有Startup.ConfigureServices 和 ConfigureServices。

這個時候我們大體猜出來了這個啟動順序是按照某種執行順序執行,且Startup.ConfigureServices 和 ConfigureServices 是同一種型別,並且他們的執行順序和他們的註冊順序保持一致。

執行順序如下:

先看一下CreateDefaultBuilder:

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) =>
	{
		var env = hostingContext.HostingEnvironment;

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

		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) =>
	{
		var 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();
		}
	})
	.UseDefaultServiceProvider((context, options) =>
	{
		var isDevelopment = context.HostingEnvironment.IsDevelopment();
		options.ValidateScopes = isDevelopment;
		options.ValidateOnBuild = isDevelopment;
	});

	return builder;
}

那麼先看下ConfigureWebDefaults 到底幹了什麼。

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

ConfigureWebDefaults 配置了一些必要的元件。

CreateDefaultBuilder 和 ConfigureWebDefaults 兩者可以看到其實就是做一些預設配置讓我們的服務能夠啟動起來。

而後續的配置也就是註冊的順序執行,只是我們的執行順序晚點罷了。

那麼再整理一下,也就是說從原理的執行順序來說是這樣的。

ConfigureHostConfiguration

ConfigureAppConfiguration

ConfigureServices

ConfigureLogging

Configure

以上都是推論。那麼直接看原始碼,因為是構建者模式,所以直接看build。

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

上面的順序和猜想是一致的。

這裡看下BuildHostConfiguration:

private List<Action<IConfigurationBuilder>> _configureHostConfigActions = new List<Action<IConfigurationBuilder>>();
private void BuildHostConfiguration()
{
	var configBuilder = new ConfigurationBuilder()
		.AddInMemoryCollection(); // Make sure there's some default storage since there are no default providers

	foreach (var buildAction in _configureHostConfigActions)
	{
		buildAction(configBuilder);
	}
	_hostConfiguration = configBuilder.Build();
}

那麼其實我們按照這種配置,那麼其實我們的ConfigureHostConfiguration全部方法都是在_configureHostConfigActions 中,到了builder 的時候就按照我們的註冊順序執行。

那麼這裡就能解釋ConfigureWebHostDefaults 不管順序都是在最前面,而裡面startup的順序確不一樣。

程式碼解釋一波:

public static IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure)
{
	return builder.ConfigureWebHost(webHostBuilder =>
	{
		WebHost.ConfigureWebDefaults(webHostBuilder);

		configure(webHostBuilder);
	});
}

ConfigureWebHostDefaults 立即執行,那麼log自然就先列印出來了,而其他幾個都是延遲執行的。他們的執行順序和註冊順序有關。

最後再解釋一下,webBuilder.UseStartup(); 到底做了什麼?

public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType)
{
	var startupAssemblyName = startupType.GetTypeInfo().Assembly.GetName().Name;

	hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName);

	// Light up the GenericWebHostBuilder implementation
	if (hostBuilder is ISupportsStartup supportsStartup)
	{
		return supportsStartup.UseStartup(startupType);
	}

	return hostBuilder
		.ConfigureServices(services =>
		{
			if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo()))
			{
				services.AddSingleton(typeof(IStartup), startupType);
			}
			else
			{
				services.AddSingleton(typeof(IStartup), sp =>
				{
					var hostingEnvironment = sp.GetRequiredService<IHostEnvironment>();
					return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName));
				});
			}
		});
}

解釋一下IsAssignableFrom:

bool res = {TypeA}.IsAssignableFrom({TypeB}) ;

如果TypeA和TypeB型別一樣則返回true;

如果TypeA是TypeB的父類則返回true;

如果TypeB實現了介面TypeA則返回true;

可以看到這裡做的是一個IStartup 到 Startup的一個依賴注入,且是單例模式,所以肯定的是Startup 裡面的方法會在ConfigureHostConfiguration、ConfigureAppConfiguration之後執行,因為其在ConfigureServices 才開始注入依賴的。

因為是應用篇,單純是一些應用需要知道的,所以比較粗糙,具體的詳細在原理篇介紹了。上述只是個人的理解,如果錯誤,望請指點,謝謝。

相關文章