Minimal API僅僅是在基於IHost/IHostBuilder的服務承載系統上作了小小的封裝而已,它利用WebApplication和WebApplicationBuilder這兩個類型提供了更加簡潔的API,同時提供了與現有API的兼容。要成分理解Minimal API的實現原理,得先對服務承載系統有基本的理解,對此不了解的可以參閱《服務承載模型[上篇]》、《服務承載模型[下篇]》、《承載服務啟動流程[上篇]》和《承載服務啟動流程[下篇]》。對於本篇提供的模擬代碼,可以從這里下載。
一、基礎模型
二、WebApplication
三、WebApplication的構建
1. BootstrapHostBuilder
2. ConfigureHostBuilder
3. ConfigureWebHostBuilder
4. WebApplicationBuilder
四、 工廠方法
一、基礎模型
對於由WebApplication和WebApplicationBuilder構建的承載模型,我們沒有必要了解其實現的每一個細節,知道其大致的設計和實現原理就可以了,所以本節會采用最簡潔的代碼模擬這兩個類型的實現。如圖1所示,代表承載應用的WebApplication對象是對一個IHost對象的封裝,而且該類型自身也實現了IHost接口, WebApplication對象其實還是作為一個IHost對象被啟動的。作為構建這的WebApplicationBuilder則是對一個IHostBuilder對象的封裝,它對WebApplication對象的構建體現在利用封裝的IHostBuilder對象構建一個對應的IHost對象,最終利用后者將WebApplication對象創建出來。
圖17-8 完整的請求處理管道
二、WebApplication
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; 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初始化的時候,它除了會創建這個HostBuilder對象,還會創建存儲服務注冊的IServiceCollection對象,以及用來對配置進行設置的ConfigurationManager對象。接下來它會創建一個BootstrapHostBuilder對象,並將它參數調用相應的方法(比如ConfigureWebHostDefaults方法)將初始化設置收集起來,收集的服務注冊和針對配置系統的設置分別轉移到創建的IServiceCollection和ConfigurationManager對象中,其他設置直接應用到封裝的HostBuilder對象上。
圖2 HostBuilder構建模型
WebApplicationBuilder在此之后會創建出代表承載環境的IWebHostEnvironment對象,並對Environment屬性進行初始化。在得到表示承載上下文的WebHostBuilderContext對象之后,上述的ConfigureHostBuilder和ConfigureWebHostBuilder對象被創建出來,並賦值給Host和WebHost屬性。與BootstrapHostBuilder作用類似,我們利用這兩個對象所作的設置最終都會轉移到上述的三個對象中。當WebApplicationBuilder進行WebApplication對象構建的時候,IServiceCollection對象存儲的服務注冊和ConfigurationManager對象承載配置最終轉移到HostBuilder對象上。此時再利用后者構建出對應的IHost對象,代表承載應用的WebApplication對象最終由該對象構建出來。
1. BootstrapHostBuilder
如下所示的是我們模擬的BootstrapHostBuilder類型的定義。正如上面所說,這個它的作用是收集初始化IHostBuilder對象提供的設置並將它們分別應用到指定的IServiceCollection、ConfigurationManager和IHostBuilder對象上。這一使命體現在BootstrapHostBuilder的Apply方法上,該方法還通過一個輸出參數返回創建的HostBuilderContext上下文。
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
ConfigureHostBuilder是在應用了BootstrapHostBuilder收集的初始化設置之后創建的,在創建該對象時提供了HostBuilderContext上下文, ConfigurationManager和IServiceCollection對象。提供的服務注冊直接添加到IServiceCollection對象中,針對配置的設置已經應用到ConfigurationManager對象,直接針對IHostBuilder對象的設置則利用_configureActions字段暫存起來。
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對象上。
3. ConfigureWebHostBuilder
ConfigureWebHostBuilder同樣是在應用了BootstrapHostBuilder提供的初始化設置后創建的,創建該對象時能夠提供WebHostBuilderContext上下文和承載配置和服務注冊的ConfigurationManager和IServiceCollection對象。由於IWebHostBuilder接口定義的方法只涉及服務注冊和針對配置的設置,所以方法提供的委托對象可以直接應用到這兩個對象上。
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類型的異常。
4. WebApplicationBuilder
如下的代碼片段模擬了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方法構建的中間件管道作為請求處理器。
四、 工廠方法
代表承載應用的WebApplication對象由WebApplicationBuilder構建的,但是我們一般不會通過調用構造函數的方式來創建WebApplicationBuilder對象,這有違“面向接口”編程的原則,所以我們都會使用WebApplication類型提供的靜態工廠方法來創建它。WebApplication除了提供了三個用於創建WebApplicationBuilder的CreateBuilder方法重載,還提供了一個直接創建WebApplication對象的Create方法。
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();