WebApplication类型不仅仅实现了IHost接口,还同时实现IApplicationBuilder接口,所以中间件可以直接注册到这个对象上的。该类型还实现了IEndpointRouteBuilder接口,所以我们还能利用它进行路由注册,我们在20章才会涉及到路由,所以我们现在先忽略针对该接口的实现。
下面的代码模拟WebApplication类型的实现。如代码片段所示,WebApplication的构造函数定义了一个IHost类型的参数,它利用这个对象完成了对IHost接口所有成员的实现,针对IApplicationBuilder接口成员的实现则利用创建的ApplicationBuilder对象来完成。WebApplication还提供了一个BuildRequestDelegate方法利用这个ApplicationBuilder对象完成了对中间件管道的构建。
public class WebApplication : IApplicationBuilder, IHost { private readonly IHost _host; private readonly ApplicationBuilder _app; // 堆代码 duidaima.com public WebApplication(IHost host) { _host = host; _app = new ApplicationBuilder(host.Services); } IServiceProvider IHost.Services => _host.Services; Task IHost.StartAsync(CancellationToken cancellationToken) => _host.StartAsync(cancellationToken); Task IHost.StopAsync(CancellationToken cancellationToken) => _host.StopAsync(cancellationToken); IServiceProvider IApplicationBuilder.ApplicationServices { get => _app.ApplicationServices; set => _app.ApplicationServices = value; } IFeatureCollection IApplicationBuilder.ServerFeatures => _app.ServerFeatures; IDictionary<string, object?> IApplicationBuilder.Properties => _app.Properties; RequestDelegate IApplicationBuilder.Build() => _app.Build(); IApplicationBuilder IApplicationBuilder.New() => _app.New(); IApplicationBuilder IApplicationBuilder .Use(Func<RequestDelegate, RequestDelegate> middleware) => _app.Use(middleware); void IDisposable.Dispose() => _host.Dispose(); public IServiceProvider Services => _host.Services; internal RequestDelegate BuildRequestDelegate() => _app.Build(); ... }WebApplication额外定义了如下的RunAsync和Run方法,它们分别以异步和同步方式启动承载的应用。调用这两个方法的时候可以指定监听地址,指定的地址被添加到IServerAddressesFeature特性中,而服务器正式利用这个特性来提供监听地址的。
public class WebApplication : IApplicationBuilder, IHost { private readonly IHost _host; public ICollection<string> Urls => _host.Services.GetRequiredService<IServer>() .Features.Get<IServerAddressesFeature>()?.Addresses ?? throw new InvalidOperationException("IServerAddressesFeature is not found."); public Task RunAsync(string? url = null) { Listen(url); return HostingAbstractionsHostExtensions.RunAsync(this); } public void Run(string? url = null) { Listen(url); HostingAbstractionsHostExtensions.Run(this); } private void Listen(string? url) { if (url is not null) { var addresses = _host.Services .GetRequiredService<IServer>().Features .Get<IServerAddressesFeature>()?.Addresses ?? throw new InvalidOperationException("IServerAddressesFeature is not found."); addresses.Clear(); addresses.Add(url); } } ... }三、WebApplication的构建
要创建一个WebApplication对象,只需要提供一个对应的IHost对象即可。IHost对象是通过IHostBuilder对象构建的,所以WebApplicationBuilder需要一个IHostBuilder对象,具体来说是一个HostBuilder对象。我们针对WebApplicationBuilder对象所作的一切设置最终都需要转移到这个HostBuilder对象上才能生效。为了提供更加简洁的API,WebApplicationBuilder类型提供了一系列的属性。
比如它利用Serrvices属性提供了可以直接进行服务注册的IServiceCollection集合,利用Environment属性提供了表示当前承载环境的IWebHostEnvironment对象,利用Configuration属性提供的ConfigurationManager对象不仅可以作为IConfigurationBuilder对象帮助我们完成对配置系统的一切设置,它自身也可以作为IConfiguration对象为我们提供配置。
WebApplicationBuilder还定义了Host和WebHost属性,对应类型为ConfigureHostBuilder和ConfigureWebHostBuilder,它们分别实现了IHostBuilder和IWebHostBuilder接口,其目的是为了复用IHostBuilder和IWebHostBuilder接口承载的API(主要是扩展方法)。
为了会尽可能使用现有方法对IHostBuilder对象进行初始化设置,它还使用了一个实现了IHostBuilder接口的BootstrapHostBuilder类型。有这些对象组成了WebApplicationBuilder针对HostBuilder的构建模型。如图2所示,WebApplicationBuilder的所有工作都是为了构建它封装的HostBuilder对象。
WebApplicationBuilder在此之后会创建出代表承载环境的IWebHostEnvironment对象,并对Environment属性进行初始化。在得到表示承载上下文的WebHostBuilderContext对象之后,上述的ConfigureHostBuilder和ConfigureWebHostBuilder对象被创建出来,并赋值给Host和WebHost属性。
与BootstrapHostBuilder作用类似,我们利用这两个对象所作的设置最终都会转移到上述的三个对象中。当WebApplicationBuilder进行WebApplication对象构建的时候,IServiceCollection对象存储的服务注册和ConfigurationManager对象承载配置最终转移到HostBuilder对象上。此时再利用后者构建出对应的IHost对象,代表承载应用的WebApplication对象最终由该对象构建出来。
public class BootstrapHostBuilder : IHostBuilder { private readonly List<Action<IConfigurationBuilder>> _configureHostConfigurations = new(); private readonly List<Action<HostBuilderContext, IConfigurationBuilder>> _configureAppConfigurations = new(); private readonly List<Action<HostBuilderContext, IServiceCollection>> _configureServices = new(); private readonly List<Action<IHostBuilder>> _others = new(); public IDictionary<object, object> Properties { get; } = new Dictionary<object, object>(); public IHost Build() => throw new NotImplementedException(); public IHostBuilder ConfigureHostConfiguration( Action<IConfigurationBuilder> configureDelegate) { _configureHostConfigurations.Add(configureDelegate); return this; } public IHostBuilder ConfigureAppConfiguration( Action<HostBuilderContext, IConfigurationBuilder> configureDelegate) { _configureAppConfigurations.Add(configureDelegate); return this; } public IHostBuilder ConfigureServices( Action<HostBuilderContext, IServiceCollection> configureDelegate) { _configureServices.Add(configureDelegate); return this; } public IHostBuilder UseServiceProviderFactory<TContainerBuilder>( IServiceProviderFactory<TContainerBuilder> factory) { _others.Add(builder => builder.UseServiceProviderFactory(factory)); return this; } public IHostBuilder UseServiceProviderFactory<TContainerBuilder>( Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory) { _others.Add(builder => builder.UseServiceProviderFactory(factory)); return this; } public IHostBuilder ConfigureContainer<TContainerBuilder>( Action<HostBuilderContext, TContainerBuilder> configureDelegate) { _others.Add(builder => builder.ConfigureContainer(configureDelegate)); return this; } internal void Apply( IHostBuilder hostBuilder, ConfigurationManager configuration, IServiceCollection services, out HostBuilderContext builderContext) { // 初始化针对宿主的配置 var hostConfiguration = new ConfigurationManager(); _configureHostConfigurations.ForEach( it => it(hostConfiguration)); // 创建承载环境 var environment = new HostingEnvironment() { ApplicationName = hostConfiguration[HostDefaults.ApplicationKey], EnvironmentName = hostConfiguration[HostDefaults.EnvironmentKey] ?? Environments.Production, ContentRootPath = HostingPathResolver.ResolvePath(hostConfiguration[HostDefaults.ContentRootKey]) }; environment.ContentRootFileProvider = new PhysicalFileProvider(environment.ContentRootPath); // 创建HostBuilderContext上下文 var hostContext = new HostBuilderContext(Properties) { Configuration = hostConfiguration, HostingEnvironment = environment, }; // 将针对宿主的配置添加到ConfigurationManager中 configuration.AddConfiguration(hostConfiguration, true); // 初始化针对应用的配置 _configureAppConfigurations.ForEach( it => it(hostContext, configuration)); // 收集服务注册 _configureServices.ForEach( it => it(hostContext, services)); // 将针对依赖注入容器的设置应用到指定的IHostBuilder对象上 _others.ForEach(it => it(hostBuilder)); // 将自定义属性转移到指定的IHostBuilder对象上 foreach (var kv in Properties) { hostBuilder.Properties[kv.Key] = kv.Value; } builderContext = hostContext; } }除了Build方法,IHostBuilder接口中定义的所有方法的参数都是委托,所以实现的这些方法将提供的委托收集起来。在Apply方法中,我们通过执行这些委托对象,将初始化设置应用到指定的IServiceCollection、ConfigurationManager和IHostBuilder对象上,并根据初始化宿主配置构建出代表承载环境的HostingEnvironment对象。该方法最后根据承载环境结合配置将HostBuilderContext上下文创建出来,并以输出参数的形式返回。
internal static class HostingPathResolver { public static string ResolvePath(string? contentRootPath) => ResolvePath(contentRootPath, .BaseDirectory); public static string ResolvePath(string? contentRootPath, string basePath) => string.IsNullOrEmpty(contentRootPath) ? Path.GetFullPath(basePath): Path.IsPathRooted(contentRootPath)? Path.GetFullPath(contentRootPath) : Path.GetFullPath(Path.Combine(Path.GetFullPath(basePath), contentRootPath)); }2. ConfigureHostBuilder
public class ConfigureHostBuilder : IHostBuilder { private readonly ConfigurationManager _configuration; private readonly IServiceCollection _services; private readonly HostBuilderContext _context; private readonly List<Action<IHostBuilder>> _configureActions = new(); internal ConfigureHostBuilder( HostBuilderContext context, ConfigurationManager configuration, IServiceCollection services) { _configuration = configuration; _services = services; _context = context; } public IDictionary<object, object> Properties => _context.Properties; public IHost Build() => throw new NotImplementedException(); public IHostBuilder ConfigureAppConfiguration( Action<HostBuilderContext, IConfigurationBuilder> configureDelegate) => Configure(() => configureDelegate(_context, _configuration)); public IHostBuilder ConfigureHostConfiguration( Action<IConfigurationBuilder> configureDelegate) { var applicationName = _configuration[HostDefaults.ApplicationKey]; var contentRoot = _context.HostingEnvironment.ContentRootPath; var environment = _configuration[HostDefaults.EnvironmentKey]; configureDelegate(_configuration); // 与环境相关的三个配置不允许改变 Validate(applicationName, HostDefaults.ApplicationKey, "Application name cannot be changed."); Validate(contentRoot, HostDefaults.ContentRootKey, "Content root cannot be changed."); Validate(environment, HostDefaults.EnvironmentKey, "Environment name cannot be changed."); return this; void Validate(string previousValue, string key, string message) { if (!string.Equals(previousValue, _configuration[key], StringComparison.OrdinalIgnoreCase)) { throw new NotSupportedException(message); } } } public IHostBuilder ConfigureServices( Action<HostBuilderContext, IServiceCollection> configureDelegate) => Configure(() => configureDelegate(_context, _services)); public IHostBuilder UseServiceProviderFactory<TContainerBuilder>( IServiceProviderFactory<TContainerBuilder> factory) => Configure(() => _configureActions.Add( b => b.UseServiceProviderFactory(factory))); public IHostBuilder UseServiceProviderFactory<TContainerBuilder>( Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory) => Configure(() => _configureActions.Add( b => b.UseServiceProviderFactory(factory))); public IHostBuilder ConfigureContainer<TContainerBuilder>( Action<HostBuilderContext, TContainerBuilder> configureDelegate) => Configure(() => _configureActions.Add( b => b.ConfigureContainer(configureDelegate))); private IHostBuilder Configure(Action configure) { configure(); return this; } internal void Apply(IHostBuilder hostBuilder) => _configureActions.ForEach(op => op(hostBuilder)); }WebApplicationBuilder对象一旦被创建出来后,针对承载环境的配置是不能改变的,所以ConfigureHostBuilder的ConfigureHostConfiguration方法针对此添加了相应的验证。两个UseServiceProviderFactory方法和ConfigureContainer方法针对依赖注入容器的设置最终需要应用到IHostBuilder对象上,所以我们将方法中提供的委托对象利用configureActions字段存起来,并最终利用Apply方法应用到指定的IHostBuilder对象上。
public class ConfigureWebHostBuilder : IWebHostBuilder, ISupportsStartup { private readonly WebHostBuilderContext _builderContext; private readonly IServiceCollection _services; private readonly ConfigurationManager _configuration; public ConfigureWebHostBuilder( WebHostBuilderContext builderContext, ConfigurationManager configuration, IServiceCollection services) { _builderContext = builderContext; _services = services; _configuration = configuration; } public IWebHost Build() => throw new NotImplementedException(); public IWebHostBuilder ConfigureAppConfiguration( Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate) => Configure(() => configureDelegate(_builderContext, _configuration)); public IWebHostBuilder ConfigureServices( Action<IServiceCollection> configureServices) => Configure(() => configureServices(_services)); public IWebHostBuilder ConfigureServices( Action<WebHostBuilderContext, IServiceCollection> configureServices) => Configure(() => configureServices(_builderContext, _services)); public string? GetSetting(string key) => _configuration[key]; public IWebHostBuilder UseSetting(string key, string? value) => Configure(() => _configuration[key] = value); IWebHostBuilder ISupportsStartup.UseStartup(Type startupType) => throw new NotImplementedException(); IWebHostBuilder ISupportsStartup.UseStartup<TStartup>( Func<WebHostBuilderContext, TStartup> startupFactory) => throw new NotImplementedException(); IWebHostBuilder ISupportsStartup.Configure( Action<IApplicationBuilder> configure) => throw new NotImplementedException(); IWebHostBuilder ISupportsStartup.Configure( Action<WebHostBuilderContext, IApplicationBuilder> configure) => throw new NotImplementedException(); private IWebHostBuilder Configure(Action configure) { configure(); return this; } }我们在前面说过,传统承载方式将初始化操作定义在注册的Startup类型的编程方式在Minima API中已经不再被支持了,所以WebApplicationBuilder本不该实现ISupportsStartup接口,但是我们希望用户在采用这种编程方式时得到显式的提醒,所以依然让它实现该接口,并在实现的方法中抛出NotImplementedException类型的异常。
如下的代码片段模拟了WebApplicationBuilder针对WebApplication的构建。它的构造函数会创建一个BootstrapHostBuilder对象,调用它的ConfigureDefaults和ConfigureWebHostDefaults扩展方法将初始化设置收集起来。
ConfigureWebHostDefaults方法会利用提供的Action<IWebHostBuilder>委托进行中间件的注册,由于中间件的注册被转移到WebApplication对象上,并且它提供了一个BuildRequestDelegate方法返回由注册中间件组成的管道,所以在这里只需调用构建的WebApplication对象(通过_application字段表示,虽然此时尚未创建,但是中间件真正被注册时会被创建出来)的这个方法,并将返回的RequestDelegate对象作为参数调用IApplicationBuilder接口的Run方法将中间件管道注册为请求处理器。
public class WebApplicationBuilder { private readonly HostBuilder _hostBuilder = new HostBuilder(); private WebApplication _application; public ConfigurationManager Configuration { get; } = new ConfigurationManager(); public IServiceCollection Services { get; } = new ServiceCollection(); public IWebHostEnvironment Environment { get; } public ConfigureHostBuilder Host { get; } public ConfigureWebHostBuilder WebHost { get; } public ILoggingBuilder Logging { get; } public WebApplicationBuilder(WebApplicationOptions options) { //创建BootstrapHostBuilder并利用它收集初始化过程中设置的配置、服务和针对依赖注入容器的设置 var args = options.Args; var bootstrap = new BootstrapHostBuilder(); bootstrap .ConfigureDefaults(null) .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder .Configure(app => app.Run(_application.BuildRequestDelegate()))) .ConfigureHostConfiguration(config => { // 添加命令行配置源 if (args?.Any() == true) { config.AddCommandLine(args); } // 将WebApplicationOptions配置选项转移到配置中 Dictionary<string, string>? settings = null; if (options.EnvironmentName is not null) (settings ??= new())[HostDefaults.EnvironmentKey] = options.EnvironmentName; if (options.ApplicationName is not null) (settings ??= new())[HostDefaults.ApplicationKey] = options.ApplicationName; if (options.ContentRootPath is not null) (settings ??= new())[HostDefaults.ContentRootKey] = options.ContentRootPath; if (options.WebRootPath is not null) (settings ??= new())[WebHostDefaults.WebRootKey] = options.EnvironmentName; if (settings != null) { config.AddInMemoryCollection(settings); } }); // 将BootstrapHostBuilder收集到配置和服务转移到Configuration和Services上 // 将应用到BootstrapHostBuilder上针对依赖注入溶质的设置转移到_hostBuilder上 // 得到BuilderContext上下文 bootstrap.Apply(_hostBuilder, Configuration, Services, out var builderContext); // 如果提供了命令行参数,在Configuration上添加对应配置源 if (options.Args?.Any() == true) { Configuration.AddCommandLine(options.Args); } // 构建WebHostBuilderContext上下文 // 初始化Host、WebHost和Logging属性 var webHostContext = (WebHostBuilderContext)builderContext .Properties[typeof(WebHostBuilderContext)]; Environment = webHostContext.HostingEnvironment; Host = new ConfigureHostBuilder( builderContext, Configuration, Services); WebHost = new ConfigureWebHostBuilder( webHostContext, Configuration, Services); Logging = new LogginigBuilder(Services); } public WebApplication Build() { // 将ConfigurationManager的配置转移到_hostBuilder _hostBuilder.ConfigureAppConfiguration(builder => { builder.AddConfiguration(Configuration); foreach (var kv in ((IConfigurationBuilder)Configuration).Properties) { builder.Properties[kv.Key] = kv.Value; } }); // 将添加的服务注册转移到_hostBuilder _hostBuilder.ConfigureServices((_, services) => { foreach (var service in Services) { services.Add(service); } }); // 将应用到Host属性上的设置转移到_hostBuilder Host.Apply(_hostBuilder); // 利用_hostBuilder构建的IHost对象创建WebApplication return _application = new WebApplication(_hostBuilder.Build()); } }
接下来BootstrapHostBuilder的ConfigureHostConfiguration方法被调用,我们利用它将提供的WebApplicationOptions配置选项转移到BootstrapHostBuilder针对宿主的配置上。针对IHostBuilder初始化设置应用到BootstrapHostBuilder对象上之后,我们调用其Apply方法将这些设置分别转移到承载服务注册和配置的IServiceCollection和ConfigurationManager对象,以及封装的HostBuilder对象上。
Apply方法利用输出参数提供了HostBuilderContext上下文,我们进一步从中提取出WebHostBuilderContext上下文(GenericWebHostBuilder会将构建的WebHostBuilderContext上下文置于HostBuilderContext对象的属性字典中)。我们利用这个上下文将ConfigureHostBuilder和ConfigureWebHostBuilder对象创建出来,并作为Host和WebHost属性。用于对日志做进一步设置的Logging属性也在这里被初始化,返回的LoggingBuilder对象仅仅是对IServiceCollection对象的简单封装而已。
构建WebApplication对象的Build方法分别调用ConfigureAppConfiguration和ConfigureServices方法将ConfigurationManager和IServiceCollection对象承载的配置和服务注册转移到HostBuilder对象上。它接下来提取出Host属性返回的ConfigureHostBuilder对象,并调用其Apply方法将应用在该对象上针对依赖注入容器的设置转移到HostBuilder对象上。
至此所有的设置全部转移到了HostBuilder对象上,我们调用其Build方法构建出对应的IHost对象后,最后利用后者将代码承载应用的WebApplication对象构建出来。我们将这个对象赋值到_application字段上,前面调用ConfigureWebHostDefaults扩展方法提供的委托会将它的BuildRequestDelegate方法构建的中间件管道作为请求处理器。
public sealed class WebApplication { public static WebApplicationBuilder CreateBuilder() => new WebApplicationBuilder(new WebApplicationOptions()); public static WebApplicationBuilder CreateBuilder(string[] args) { var options = new WebApplicationOptions(); options.Args = args; return new WebApplicationBuilder(options); } public static WebApplicationBuilder CreateBuilder(WebApplicationOptions options) => new WebApplicationBuilder(options, null); public static WebApplication Create(string[]? args = null) { var options = new WebApplicationOptions(); options.Args = args; return new WebApplicationBuilder(options).Build(); } }本节内容通过针对WebApplication和WebApplicationBuilder这两个类型的实现模拟来讲解Minimal API的实现原理。一方面为了让讲解更加清晰,另一方面也出于篇幅的限制,不得不省去很多细枝末节的内容,但是设计思想和实现原理别无二致。上面提供的源代码也不是伪代码,如下所示的就是在“模拟的Minimal API”构建的ASP.NET Core应用,它是可以正常运行的。如果读者朋友们对真实的实现感兴趣,可以将它作为一个“向导”去探寻“真实的Minimal API”。
var app = App.WebApplication.Create(); app.Run(httpContext => httpContext.Response.WriteAsync("Hello World!")); app.Run();