ASP.NET Core 6 基础入门系列(17) ASP.NET Core 的核心对象WebApplication与WebApplicationBuilder

  在我的博客《ASP.NET Core 6 基础入门系列(11) 项目结构详解之项目入口Program.cs》中介绍了ASP.NET Core 项目入口文件的主要内容,其中逻辑代码的第一行中用到了 WebApplicationWebApplicationBuilder 类。

WebApplicationBuilder builder = WebApplication.CreateBuilder(args); 
  ASP.NET Core 项目模板创建了 WebApplicationBuilder 和 WebApplication 类,这提供了一种简化的方式来配置和运行 Web 应用程序,这两个类在 ASP.NET Core Web 6.0+ 项目中发挥着举足轻重的作用。

  这里涉及到 ASP.NET Core 主机 的知识点,ASP.NET Core 应用配置和启动“主机”。 主机负责应用程序启动和生存期管理。 至少,主机配置服务器和请求处理管道。 主机还可以设置日志记录、依赖关系注入和配置。

  详细请参考微软文档《ASP.NET Core Web 主机

WebApplication 类

  WebApplication类主要用于配置 HTTP 管道和路由的 Web 应用程序。查看源码

ASP.NET Core 6.0 项目开源地址: https://github.com/dotnet/aspnetcore/tree/v6.0.13

WebApplication 类的完整代码如下

复制代码
  1 // Licensed to the .NET Foundation under one or more agreements.
  2 // The .NET Foundation licenses this file to you under the MIT license.
  3 
  4 using Microsoft.AspNetCore.Hosting;
  5 using Microsoft.AspNetCore.Hosting.Server;
  6 using Microsoft.AspNetCore.Hosting.Server.Features;
  7 using Microsoft.AspNetCore.Http;
  8 using Microsoft.AspNetCore.Http.Features;
  9 using Microsoft.AspNetCore.Routing;
 10 using Microsoft.Extensions.Configuration;
 11 using Microsoft.Extensions.DependencyInjection;
 12 using Microsoft.Extensions.Hosting;
 13 using Microsoft.Extensions.Logging;
 14 
 15 namespace Microsoft.AspNetCore.Builder
 16 {
 17     /// <summary>
 18     /// The web application used to configure the HTTP pipeline, and routes.
 19     /// </summary>
 20     public sealed class WebApplication : IHost, IApplicationBuilder, IEndpointRouteBuilder, IAsyncDisposable
 21     {
 22         internal const string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder";
 23 
 24         private readonly IHost _host;
 25         private readonly List<EndpointDataSource> _dataSources = new();
 26 
 27         internal WebApplication(IHost host)
 28         {
 29             _host = host;
 30             ApplicationBuilder = new ApplicationBuilder(host.Services);
 31             Logger = host.Services.GetRequiredService<ILoggerFactory>().CreateLogger(Environment.ApplicationName);
 32 
 33             Properties[GlobalEndpointRouteBuilderKey] = this;
 34         }
 35 
 36         /// <summary>
 37         /// The application's configured services.
 38         /// </summary>
 39         public IServiceProvider Services => _host.Services;
 40 
 41         /// <summary>
 42         /// The application's configured <see cref="IConfiguration"/>.
 43         /// </summary>
 44         public IConfiguration Configuration => _host.Services.GetRequiredService<IConfiguration>();
 45 
 46         /// <summary>
 47         /// The application's configured <see cref="IWebHostEnvironment"/>.
 48         /// </summary>
 49         public IWebHostEnvironment Environment => _host.Services.GetRequiredService<IWebHostEnvironment>();
 50 
 51         /// <summary>
 52         /// Allows consumers to be notified of application lifetime events.
 53         /// </summary>
 54         public IHostApplicationLifetime Lifetime => _host.Services.GetRequiredService<IHostApplicationLifetime>();
 55 
 56         /// <summary>
 57         /// The default logger for the application.
 58         /// </summary>
 59         public ILogger Logger { get; }
 60 
 61         /// <summary>
 62         /// The list of URLs that the HTTP server is bound to.
 63         /// </summary>
 64         public ICollection<string> Urls => ServerFeatures.Get<IServerAddressesFeature>()?.Addresses ??
 65             throw new InvalidOperationException($"{nameof(IServerAddressesFeature)} could not be found.");
 66 
 67         IServiceProvider IApplicationBuilder.ApplicationServices
 68         {
 69             get => ApplicationBuilder.ApplicationServices;
 70             set => ApplicationBuilder.ApplicationServices = value;
 71         }
 72 
 73         internal IFeatureCollection ServerFeatures => _host.Services.GetRequiredService<IServer>().Features;
 74         IFeatureCollection IApplicationBuilder.ServerFeatures => ServerFeatures;
 75 
 76         internal IDictionary<string, object?> Properties => ApplicationBuilder.Properties;
 77         IDictionary<string, object?> IApplicationBuilder.Properties => Properties;
 78 
 79         internal ICollection<EndpointDataSource> DataSources => _dataSources;
 80         ICollection<EndpointDataSource> IEndpointRouteBuilder.DataSources => DataSources;
 81 
 82         internal ApplicationBuilder ApplicationBuilder { get; }
 83 
 84         IServiceProvider IEndpointRouteBuilder.ServiceProvider => Services;
 85 
 86         /// <summary>
 87         /// Initializes a new instance of the <see cref="WebApplication"/> class with preconfigured defaults.
 88         /// </summary>
 89         /// <param name="args">Command line arguments</param>
 90         /// <returns>The <see cref="WebApplication"/>.</returns>
 91         public static WebApplication Create(string[]? args = null) =>
 92             new WebApplicationBuilder(new WebApplicationOptions() { Args = args }).Build();
 93 
 94         /// <summary>
 95         /// Initializes a new instance of the <see cref="WebApplicationBuilder"/> class with preconfigured defaults.
 96         /// </summary>
 97         /// <returns>The <see cref="WebApplicationBuilder"/>.</returns>
 98         public static WebApplicationBuilder CreateBuilder() =>
 99             new WebApplicationBuilder(new WebApplicationOptions());
100 
101         /// <summary>
102         /// Initializes a new instance of the <see cref="WebApplicationBuilder"/> class with preconfigured defaults.
103         /// </summary>
104         /// <param name="args">Command line arguments</param>
105         /// <returns>The <see cref="WebApplicationBuilder"/>.</returns>
106         public static WebApplicationBuilder CreateBuilder(string[] args) =>
107             new WebApplicationBuilder(new WebApplicationOptions() { Args = args });
108 
109         /// <summary>
110         /// Initializes a new instance of the <see cref="WebApplicationBuilder"/> class with preconfigured defaults.
111         /// </summary>
112         /// <param name="options">The <see cref="WebApplicationOptions"/> to configure the <see cref="WebApplicationBuilder"/>.</param>
113         /// <returns>The <see cref="WebApplicationBuilder"/>.</returns>
114         public static WebApplicationBuilder CreateBuilder(WebApplicationOptions options) =>
115             new (options);
116 
117         /// <summary>
118         /// Start the application.
119         /// </summary>
120         /// <param name="cancellationToken"></param>
121         /// <returns>
122         /// A <see cref="Task"/> that represents the startup of the <see cref="WebApplication"/>.
123         /// Successful completion indicates the HTTP server is ready to accept new requests.
124         /// </returns>
125         public Task StartAsync(CancellationToken cancellationToken = default) =>
126             _host.StartAsync(cancellationToken);
127 
128         /// <summary>
129         /// Shuts down the application.
130         /// </summary>
131         /// <param name="cancellationToken"></param>
132         /// <returns>
133         /// A <see cref="Task"/> that represents the shutdown of the <see cref="WebApplication"/>.
134         /// Successful completion indicates that all the HTTP server has stopped.
135         /// </returns>
136         public Task StopAsync(CancellationToken cancellationToken = default) =>
137             _host.StopAsync(cancellationToken);
138 
139         /// <summary>
140         /// Runs an application and returns a Task that only completes when the token is triggered or shutdown is triggered.
141         /// </summary>
142         /// <param name="url">The URL to listen to if the server hasn't been configured directly.</param>
143         /// <returns>
144         /// A <see cref="Task"/> that represents the entire runtime of the <see cref="WebApplication"/> from startup to shutdown.
145         /// </returns>
146         public Task RunAsync(string? url = null)
147         {
148             Listen(url);
149             return HostingAbstractionsHostExtensions.RunAsync(this);
150         }
151 
152         /// <summary>
153         /// Runs an application and block the calling thread until host shutdown.
154         /// </summary>
155         /// <param name="url">The URL to listen to if the server hasn't been configured directly.</param>
156         public void Run(string? url = null)
157         {
158             Listen(url);
159             HostingAbstractionsHostExtensions.Run(this);
160         }
161 
162         /// <summary>
163         /// Disposes the application.
164         /// </summary>
165         void IDisposable.Dispose() => _host.Dispose();
166 
167         /// <summary>
168         /// Disposes the application.
169         /// </summary>
170         public ValueTask DisposeAsync() => ((IAsyncDisposable)_host).DisposeAsync();
171 
172         internal RequestDelegate BuildRequestDelegate() => ApplicationBuilder.Build();
173         RequestDelegate IApplicationBuilder.Build() => BuildRequestDelegate();
174 
175         // REVIEW: Should this be wrapping another type?
176         IApplicationBuilder IApplicationBuilder.New()
177         {
178             var newBuilder = ApplicationBuilder.New();
179             // Remove the route builder so branched pipelines have their own routing world
180             newBuilder.Properties.Remove(GlobalEndpointRouteBuilderKey);
181             return newBuilder;
182         }
183 
184         IApplicationBuilder IApplicationBuilder.Use(Func<RequestDelegate, RequestDelegate> middleware)
185         {
186             ApplicationBuilder.Use(middleware);
187             return this;
188         }
189 
190         IApplicationBuilder IEndpointRouteBuilder.CreateApplicationBuilder() => ((IApplicationBuilder)this).New();
191 
192         private void Listen(string? url)
193         {
194             if (url is null)
195             {
196                 return;
197             }
198 
199             var addresses = ServerFeatures.Get<IServerAddressesFeature>()?.Addresses;
200             if (addresses is null)
201             {
202                 throw new InvalidOperationException($"Changing the URL is not supported because no valid {nameof(IServerAddressesFeature)} was found.");
203             }
204             if (addresses.IsReadOnly)
205             {
206                 throw new InvalidOperationException($"Changing the URL is not supported because {nameof(IServerAddressesFeature.Addresses)} {nameof(ICollection<string>.IsReadOnly)}.");
207             }
208 
209             addresses.Clear();
210             addresses.Add(url);
211         }
212     }
213 }
View Code
复制代码

1、WebApplication 类实现了 IApplicationBuilder IEndpointRouteBuilder IHost IAsyncDisposable IDisposable 接口。

2、Program.cs 文件中调用了 CreateBuilder(args) 方法并传入命令行参数以生成一个 WebApplicationBuilder 对象。

查看源码其内部实现如下:

        /// <summary>
        /// Initializes a new instance of the <see cref="WebApplicationBuilder"/> class with preconfigured defaults.
        /// </summary>
        /// <param name="args">Command line arguments</param>
        /// <returns>The <see cref="WebApplicationBuilder"/>.</returns>
        public static WebApplicationBuilder CreateBuilder(string[] args) =>
            new WebApplicationBuilder(new WebApplicationOptions() { Args = args });

首先将 命令行参数args 赋值给 WebApplicationOptions 类的 Args 属性,然后将生成的 WebApplicationOptions 对象作为参数传入WebApplicationBuilder 类的构造函数以生成一个 WebApplicationBuilder 对象并返回。

其中 WebApplicationOptions 类是用于配置CreateBuilder()行为的选项。查看源码,WebApplicationOptions 类的实现如下

复制代码
  1 // Licensed to the .NET Foundation under one or more agreements.
  2 // The .NET Foundation licenses this file to you under the MIT license.
  3 
  4 using System;
  5 using System.Collections.Generic;
  6 using System.Linq;
  7 using System.Reflection;
  8 using System.Text;
  9 using System.Threading.Tasks;
 10 using Microsoft.AspNetCore.Hosting;
 11 using Microsoft.Extensions.Configuration;
 12 using Microsoft.Extensions.Hosting;
 13 
 14 namespace Microsoft.AspNetCore.Builder
 15 {
 16     /// <summary>
 17     /// Options for configuing the behavior for <see cref="WebApplication.CreateBuilder(WebApplicationOptions)"/>.
 18     /// </summary>
 19     public class WebApplicationOptions
 20     {
 21         /// <summary>
 22         /// The command line arguments.
 23         /// </summary>
 24         public string[]? Args { get; init; }
 25 
 26         /// <summary>
 27         /// The environment name.
 28         /// </summary>
 29         public string? EnvironmentName { get; init; }
 30 
 31         /// <summary>
 32         /// The application name.
 33         /// </summary>
 34         public string? ApplicationName { get; init; }
 35 
 36         /// <summary>
 37         /// The content root path.
 38         /// </summary>
 39         public string? ContentRootPath { get; init; }
 40 
 41         /// <summary>
 42         /// The web root path.
 43         /// </summary>
 44         public string? WebRootPath { get; init; }
 45 
 46         internal void ApplyHostConfiguration(IConfigurationBuilder builder)
 47         {
 48             Dictionary<string, string>? config = null;
 49 
 50             if (EnvironmentName is not null)
 51             {
 52                 config = new();
 53                 config[HostDefaults.EnvironmentKey] = EnvironmentName;
 54             }
 55 
 56             if (ApplicationName is not null)
 57             {
 58                 config ??= new();
 59                 config[HostDefaults.ApplicationKey] = ApplicationName;
 60             }
 61 
 62             if (ContentRootPath is not null)
 63             {
 64                 config ??= new();
 65                 config[HostDefaults.ContentRootKey] = ContentRootPath;
 66             }
 67 
 68             if (WebRootPath is not null)
 69             {
 70                 config ??= new();
 71                 config[WebHostDefaults.WebRootKey] = WebRootPath;
 72             }
 73 
 74             if (config is not null)
 75             {
 76                 builder.AddInMemoryCollection(config);
 77             }
 78         }
 79 
 80         internal void ApplyApplicationName(IWebHostBuilder webHostBuilder)
 81         {
 82             string? applicationName = null;
 83 
 84             // We need to "parse" the args here since
 85             // we need to set the application name via UseSetting
 86             if (Args is not null)
 87             {
 88                 var config = new ConfigurationBuilder()
 89                         .AddCommandLine(Args)
 90                         .Build();
 91 
 92                 applicationName = config[WebHostDefaults.ApplicationKey];
 93 
 94                 // This isn't super important since we're not adding any disposable sources
 95                 // but just in case
 96                 if (config is IDisposable disposable)
 97                 {
 98                     disposable.Dispose();
 99                 }
100             }
101 
102             // Application name overrides args
103             if (ApplicationName is not null)
104             {
105                 applicationName = ApplicationName;
106             }
107 
108             // We need to override the application name since the call to Configure will set it to
109             // be the calling assembly's name.
110             applicationName ??= Assembly.GetEntryAssembly()?.GetName()?.Name ?? string.Empty;
111 
112             webHostBuilder.UseSetting(WebHostDefaults.ApplicationKey, applicationName);
113         }
114     }
115 }
View Code
复制代码

其核心属性

  • ApplicationName:应用程序名称
  • Args:命令行参数
  • ContentRootPath:内容根路径
  • EnvironmentName:环境名称
  • WebRootPath:Web 根路径

调试程序,查看运行结果如下,从下图可以看出,通过调用  WebApplication.CreateBuilder(args) 方法创建 WebApplicationBuilder 之后,通过 builder.Environment 属性查看项目的环境信息。使用的都是 ASP.NET Core 项目中预设的默认信息

通过 WebApplication 对象的 Environment 属性也可以查看项目的环境信息,与上述结果一致。 

通过 WebApplication.CreateBuilder(args) 创建 WebApplicationBuilder 对象之后,无法更改任何主机设置,例如应用名称、环境或内容根。

如果想实现自定义的一些配置,则需要使用 CreateBuilder() 的重载方法

查看源码,其内部实现如下

        /// <summary>
        /// Initializes a new instance of the <see cref="WebApplicationBuilder"/> class with preconfigured defaults.
        /// </summary>
        /// <param name="options">The <see cref="WebApplicationOptions"/> to configure the <see cref="WebApplicationBuilder"/>.</param>
        /// <returns>The <see cref="WebApplicationBuilder"/>.</returns>
        public static WebApplicationBuilder CreateBuilder(WebApplicationOptions options) => new (options);

通过 WebApplication.CreateBuilder(options) 实现方式如下(以下仅为测试)

 

创建 WebApplicationOptions 对象的同时,将 args 参数赋值给 Args 属性,同时根据实际需要设置其他自定义的配置信息,如设置web静态资源的存储目录为 webroot等。

调试程序,查看运行结果如下,从下图可以看出,通过调用  WebApplication.CreateBuilder(options) 方法创建 WebApplicationBuilder 对象之后,通过 builder.Environment 属性查看项目的环境信息,自定义的信息已经生效

3、调用中间件

  Program.cs 文件中 通过 执行builder.Build() 构建了一个 WebApplication 对象,然后调用丰富的中间件组件以实现不同的功能。中间件是系统内置的针对 IApplicationBuilder 接口进行扩展的方法。因为 WebApplication  继承了IApplicationBuilder 接口,所以其可以调用扩展的中间件。

WebApplicationBuilder 类

通过 WebApplication.CreateBuilder(args) 方法创建了 WebApplicationBuilder 对象, 再仔细分析CreateBuilder()方法,其内部通过初始化 WebApplicationBuilder 类的新实例并返回

1         /// <summary>
2         /// Initializes a new instance of the <see cref="WebApplicationBuilder"/> class with preconfigured defaults.
3         /// </summary>
4         /// <param name="args">Command line arguments</param>
5         /// <returns>The <see cref="WebApplicationBuilder"/>.</returns>
6         public static WebApplicationBuilder CreateBuilder(string[] args) =>
7             new WebApplicationBuilder(new WebApplicationOptions() { Args = args });
WebApplicationBuilder 类的完整实现如下
复制代码
  1 // Licensed to the .NET Foundation under one or more agreements.
  2 // The .NET Foundation licenses this file to you under the MIT license.
  3 
  4 using System.Diagnostics;
  5 using System.Linq;
  6 using Microsoft.AspNetCore.Hosting;
  7 using Microsoft.Extensions.Configuration;
  8 using Microsoft.Extensions.DependencyInjection;
  9 using Microsoft.Extensions.Hosting;
 10 using Microsoft.Extensions.Logging;
 11 
 12 namespace Microsoft.AspNetCore.Builder
 13 {
 14     /// <summary>
 15     /// A builder for web applications and services.
 16     /// </summary>
 17     public sealed class WebApplicationBuilder
 18     {
 19         private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder";
 20 
 21         private readonly HostBuilder _hostBuilder = new();
 22         private readonly BootstrapHostBuilder _bootstrapHostBuilder;
 23         private readonly WebApplicationServiceCollection _services = new();
 24         private readonly List<KeyValuePair<string, string>> _hostConfigurationValues;
 25         private readonly ConfigurationManager _hostConfigurationManager = new();
 26 
 27         private WebApplication? _builtApplication;
 28 
 29         internal WebApplicationBuilder(WebApplicationOptions options, Action<IHostBuilder>? configureDefaults = null)
 30         {
 31             Services = _services;
 32 
 33             var args = options.Args;
 34 
 35             // Run methods to configure both generic and web host defaults early to populate config from appsettings.json
 36             // environment variables (both DOTNET_ and ASPNETCORE_ prefixed) and other possible default sources to prepopulate
 37             // the correct defaults.
 38             _bootstrapHostBuilder = new BootstrapHostBuilder(Services, _hostBuilder.Properties);
 39 
 40             // Don't specify the args here since we want to apply them later so that args
 41             // can override the defaults specified by ConfigureWebHostDefaults
 42             _bootstrapHostBuilder.ConfigureDefaults(args: null);
 43 
 44             // This is for testing purposes
 45             configureDefaults?.Invoke(_bootstrapHostBuilder);
 46 
 47             // We specify the command line here last since we skipped the one in the call to ConfigureDefaults.
 48             // The args can contain both host and application settings so we want to make sure
 49             // we order those configuration providers appropriately without duplicating them
 50             if (args is { Length: > 0 })
 51             {
 52                 _bootstrapHostBuilder.ConfigureAppConfiguration(config =>
 53                 {
 54                     config.AddCommandLine(args);
 55                 });
 56             }
 57 
 58             _bootstrapHostBuilder.ConfigureWebHostDefaults(webHostBuilder =>
 59             {
 60                 // Runs inline.
 61                 webHostBuilder.Configure(ConfigureApplication);
 62 
 63                 // Attempt to set the application name from options
 64                 options.ApplyApplicationName(webHostBuilder);
 65             });
 66 
 67             // Apply the args to host configuration last since ConfigureWebHostDefaults overrides a host specific setting (the application name).
 68             _bootstrapHostBuilder.ConfigureHostConfiguration(config =>
 69             {
 70                 if (args is { Length: > 0 })
 71                 {
 72                     config.AddCommandLine(args);
 73                 }
 74 
 75                 // Apply the options after the args
 76                 options.ApplyHostConfiguration(config);
 77             });
 78 
 79             Configuration = new();
 80             // This is chained as the first configuration source in Configuration so host config can be added later without overriding app config.
 81             Configuration.AddConfiguration(_hostConfigurationManager);
 82 
 83             // Collect the hosted services separately since we want those to run after the user's hosted services
 84             _services.TrackHostedServices = true;
 85 
 86             // This is the application configuration
 87             var (hostContext, hostConfiguration) = _bootstrapHostBuilder.RunDefaultCallbacks(Configuration, _hostBuilder);
 88 
 89             // Stop tracking here
 90             _services.TrackHostedServices = false;
 91 
 92             // Capture the host configuration values here. We capture the values so that
 93             // changes to the host configuration have no effect on the final application. The
 94             // host configuration is immutable at this point.
 95             _hostConfigurationValues = new(hostConfiguration.AsEnumerable());
 96 
 97             // Grab the WebHostBuilderContext from the property bag to use in the ConfigureWebHostBuilder
 98             var webHostContext = (WebHostBuilderContext)hostContext.Properties[typeof(WebHostBuilderContext)];
 99 
100             // Grab the IWebHostEnvironment from the webHostContext. This also matches the instance in the IServiceCollection.
101             Environment = webHostContext.HostingEnvironment;
102             Logging = new LoggingBuilder(Services);
103             Host = new ConfigureHostBuilder(hostContext, Configuration, Services);
104             WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services);
105 
106             Services.AddSingleton<IConfiguration>(_ => Configuration);
107         }
108 
109         /// <summary>
110         /// Provides information about the web hosting environment an application is running.
111         /// </summary>
112         public IWebHostEnvironment Environment { get; }
113 
114         /// <summary>
115         /// A collection of services for the application to compose. This is useful for adding user provided or framework provided services.
116         /// </summary>
117         public IServiceCollection Services { get; }
118 
119         /// <summary>
120         /// A collection of configuration providers for the application to compose. This is useful for adding new configuration sources and providers.
121         /// </summary>
122         public ConfigurationManager Configuration { get; }
123 
124         /// <summary>
125         /// A collection of logging providers for the application to compose. This is useful for adding new logging providers.
126         /// </summary>
127         public ILoggingBuilder Logging { get; }
128 
129         /// <summary>
130         /// An <see cref="IWebHostBuilder"/> for configuring server specific properties, but not building.
131         /// To build after configuration, call <see cref="Build"/>.
132         /// </summary>
133         public ConfigureWebHostBuilder WebHost { get; }
134 
135         /// <summary>
136         /// An <see cref="IHostBuilder"/> for configuring host specific properties, but not building.
137         /// To build after configuration, call <see cref="Build"/>.
138         /// </summary>
139         public ConfigureHostBuilder Host { get; }
140 
141         /// <summary>
142         /// Builds the <see cref="WebApplication"/>.
143         /// </summary>
144         /// <returns>A configured <see cref="WebApplication"/>.</returns>
145         public WebApplication Build()
146         {
147             // Wire up the host configuration here. We don't try to preserve the configuration
148             // source itself here since we don't support mutating the host values after creating the builder.
149             _hostBuilder.ConfigureHostConfiguration(builder =>
150             {
151                 builder.AddInMemoryCollection(_hostConfigurationValues);
152             });
153 
154             var chainedConfigSource = new TrackingChainedConfigurationSource(Configuration);
155 
156             // Wire up the application configuration by copying the already built configuration providers over to final configuration builder.
157             // We wrap the existing provider in a configuration source to avoid re-bulding the already added configuration sources.
158             _hostBuilder.ConfigureAppConfiguration(builder =>
159             {
160                 builder.Add(chainedConfigSource);
161 
162                 foreach (var (key, value) in ((IConfigurationBuilder)Configuration).Properties)
163                 {
164                     builder.Properties[key] = value;
165                 }
166             });
167 
168             // This needs to go here to avoid adding the IHostedService that boots the server twice (the GenericWebHostService).
169             // Copy the services that were added via WebApplicationBuilder.Services into the final IServiceCollection
170             _hostBuilder.ConfigureServices((context, services) =>
171             {
172                 // We've only added services configured by the GenericWebHostBuilder and WebHost.ConfigureWebDefaults
173                 // at this point. HostBuilder news up a new ServiceCollection in HostBuilder.Build() we haven't seen
174                 // until now, so we cannot clear these services even though some are redundant because
175                 // we called ConfigureWebHostDefaults on both the _deferredHostBuilder and _hostBuilder.
176                 foreach (var s in _services)
177                 {
178                     services.Add(s);
179                 }
180 
181                 // Add the hosted services that were initially added last
182                 // this makes sure any hosted services that are added run after the initial set
183                 // of hosted services. This means hosted services run before the web host starts.
184                 foreach (var s in _services.HostedServices)
185                 {
186                     services.Add(s);
187                 }
188 
189                 // Clear the hosted services list out
190                 _services.HostedServices.Clear();
191 
192                 // Add any services to the user visible service collection so that they are observable
193                 // just in case users capture the Services property. Orchard does this to get a "blueprint"
194                 // of the service collection
195 
196                 // Drop the reference to the existing collection and set the inner collection
197                 // to the new one. This allows code that has references to the service collection to still function.
198                 _services.InnerCollection = services;
199 
200                 // Keep any configuration sources added before the TrackingChainedConfigurationSource (namely host configuration from _hostConfigurationValues)
201                 // from overriding config values set via Configuration by inserting them at beginning using _hostConfigurationValues.
202                 var beforeChainedConfig = true;
203                 var hostBuilderProviders = ((IConfigurationRoot)context.Configuration).Providers;
204 
205                 if (!hostBuilderProviders.Contains(chainedConfigSource.BuiltProvider))
206                 {
207                     // Something removed the _hostBuilder's TrackingChainedConfigurationSource pointing back to the ConfigurationManager.
208                     // This is likely a test using WebApplicationFactory. Replicate the effect by clearing the ConfingurationManager sources.
209                     ((IConfigurationBuilder)Configuration).Sources.Clear();
210                     beforeChainedConfig = false;
211                 }
212 
213                 // Make the ConfigurationManager match the final _hostBuilder's configuration. To do that, we add the additional providers
214                 // to the inner _hostBuilders's configuration to the ConfigurationManager. We wrap the existing provider in a
215                 // configuration source to avoid rebuilding or reloading the already added configuration sources.
216                 foreach (var provider in hostBuilderProviders)
217                 {
218                     if (ReferenceEquals(provider, chainedConfigSource.BuiltProvider))
219                     {
220                         beforeChainedConfig = false;
221                     }
222                     else
223                     {
224                         IConfigurationBuilder configBuilder = beforeChainedConfig ? _hostConfigurationManager : Configuration;
225                         configBuilder.Add(new ConfigurationProviderSource(provider));
226                     }
227                 }
228             });
229 
230             // Run the other callbacks on the final host builder
231             Host.RunDeferredCallbacks(_hostBuilder);
232 
233             _builtApplication = new WebApplication(_hostBuilder.Build());
234 
235             // Mark the service collection as read-only to prevent future modifications
236             _services.IsReadOnly = true;
237 
238             // Resolve both the _hostBuilder's Configuration and builder.Configuration to mark both as resolved within the
239             // service provider ensuring both will be properly disposed with the provider.
240             _ = _builtApplication.Services.GetService<IEnumerable<IConfiguration>>();
241 
242             return _builtApplication;
243         }
244 
245         private void ConfigureApplication(WebHostBuilderContext context, IApplicationBuilder app)
246         {
247             Debug.Assert(_builtApplication is not null);
248 
249             // UseRouting called before WebApplication such as in a StartupFilter
250             // lets remove the property and reset it at the end so we don't mess with the routes in the filter
251             if (app.Properties.TryGetValue(EndpointRouteBuilderKey, out var priorRouteBuilder))
252             {
253                 app.Properties.Remove(EndpointRouteBuilderKey);
254             }
255 
256             if (context.HostingEnvironment.IsDevelopment())
257             {
258                 app.UseDeveloperExceptionPage();
259             }
260 
261             // Wrap the entire destination pipeline in UseRouting() and UseEndpoints(), essentially:
262             // destination.UseRouting()
263             // destination.Run(source)
264             // destination.UseEndpoints()
265 
266             // Set the route builder so that UseRouting will use the WebApplication as the IEndpointRouteBuilder for route matching
267             app.Properties.Add(WebApplication.GlobalEndpointRouteBuilderKey, _builtApplication);
268 
269             // Only call UseRouting() if there are endpoints configured and UseRouting() wasn't called on the global route builder already
270             if (_builtApplication.DataSources.Count > 0)
271             {
272                 // If this is set, someone called UseRouting() when a global route builder was already set
273                 if (!_builtApplication.Properties.TryGetValue(EndpointRouteBuilderKey, out var localRouteBuilder))
274                 {
275                     app.UseRouting();
276                 }
277                 else
278                 {
279                     // UseEndpoints will be looking for the RouteBuilder so make sure it's set
280                     app.Properties[EndpointRouteBuilderKey] = localRouteBuilder;
281                 }
282             }
283 
284             // Wire the source pipeline to run in the destination pipeline
285             app.Use(next =>
286             {
287                 _builtApplication.Run(next);
288                 return _builtApplication.BuildRequestDelegate();
289             });
290 
291             if (_builtApplication.DataSources.Count > 0)
292             {
293                 // We don't know if user code called UseEndpoints(), so we will call it just in case, UseEndpoints() will ignore duplicate DataSources
294                 app.UseEndpoints(_ => { });
295             }
296 
297             // Copy the properties to the destination app builder
298             foreach (var item in _builtApplication.Properties)
299             {
300                 app.Properties[item.Key] = item.Value;
301             }
302 
303             // Remove the route builder to clean up the properties, we're done adding routes to the pipeline
304             app.Properties.Remove(WebApplication.GlobalEndpointRouteBuilderKey);
305 
306             // reset route builder if it existed, this is needed for StartupFilters
307             if (priorRouteBuilder is not null)
308             {
309                 app.Properties[EndpointRouteBuilderKey] = priorRouteBuilder;
310             }
311         }
312 
313         private sealed class LoggingBuilder : ILoggingBuilder
314         {
315             public LoggingBuilder(IServiceCollection services)
316             {
317                 Services = services;
318             }
319 
320             public IServiceCollection Services { get; }
321         }
322     }
323 }
View Code
复制代码

分析 WebApplicationBuilder 类的构造函数

复制代码
 1         internal WebApplicationBuilder(WebApplicationOptions options, Action<IHostBuilder>? configureDefaults = null)
 2         {
 3             Services = _services;
 4 
 5             var args = options.Args;
 6 
 7             // 运行方法以尽早配置通用和web主机默认值,以从appsettings.json环境变量(以DOTNET_和ASPNETCORE_为前缀)和其他可能的默认源填充config,以预填充正确的默认值。
 8             _bootstrapHostBuilder = new BootstrapHostBuilder(Services, _hostBuilder.Properties);
 9 
10             // 不要在此处指定参数,因为我们希望稍后应用它们可以覆盖ConfigureWebHostDefaults指定的默认值
11             _bootstrapHostBuilder.ConfigureDefaults(args: null);
12 
13             // 这是为了测试目的
14             configureDefaults?.Invoke(_bootstrapHostBuilder);
15 
16             // 上次在这里指定了命令行,因为跳过了 ConfigureDefaults 调用中的命令行。
17             // 参数可以包含主机和应用程序设置,因此我们希望确保正确订购这些配置提供程序,而不复制它们
18             if (args is { Length: > 0 })
19             {
20                 _bootstrapHostBuilder.ConfigureAppConfiguration(config =>
21                 {
22                     config.AddCommandLine(args);
23                 });
24             }
25 
26             _bootstrapHostBuilder.ConfigureWebHostDefaults(webHostBuilder =>
27             {
28                 // 内联运行
29                 webHostBuilder.Configure(ConfigureApplication);
30 
31                 // 尝试从选项设置应用程序名称
32                 options.ApplyApplicationName(webHostBuilder);
33             });
34 
35             // 由于ConfigureWebHostDefaults覆盖特定于主机的设置(应用程序名称),最后将参数应用于主机配置。
36             _bootstrapHostBuilder.ConfigureHostConfiguration(config =>
37             {
38                 if (args is { Length: > 0 })
39                 {
40                     config.AddCommandLine(args);
41                 }
42 
43                 // 在参数后应用选项
44                 options.ApplyHostConfiguration(config);
45             });
46 
47             Configuration = new();
48             // 这是作为配置中的第一个配置源链接的,因此可以稍后添加主机配置,而无需覆盖应用程序配置。
49             Configuration.AddConfiguration(_hostConfigurationManager);
50 
51             // 单独收集托管服务,因为我们希望这些服务在用户的托管服务之后运行
52             _services.TrackHostedServices = true;
53 
54             // 这是应用程序配置
55             var (hostContext, hostConfiguration) = _bootstrapHostBuilder.RunDefaultCallbacks(Configuration, _hostBuilder);
56 
57             // 此处停止跟踪
58             _services.TrackHostedServices = false;
59 
60             //在此捕获主机配置值。我们捕获这些值,以便对主机配置的更改不会对最终应用程序产生影响。此时主机配置是不可变的。
61             _hostConfigurationValues = new(hostConfiguration.AsEnumerable());
62 
63             // 从属性包中获取 WebHostBuilderContext 以在 ConfigureWebHostBuilder 中使用
64             var webHostContext = (WebHostBuilderContext)hostContext.Properties[typeof(WebHostBuilderContext)];
65 
66             // 从webHostContext中获取IWebHostEnvironment。这也与IServiceCollection中的实例匹配。
67             Environment = webHostContext.HostingEnvironment;
68             Logging = new LoggingBuilder(Services);
69             Host = new ConfigureHostBuilder(hostContext, Configuration, Services);
70             WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services);
71 
72             Services.AddSingleton<IConfiguration>(_ => Configuration);
73         }
复制代码

1、其内部完成如下工作

(1)第3行,通过 WebApplicationServiceCollection _services = new() 方式创建的服务容器赋值给 IServiceCollection 类型的 Services 属性。

Progarm.cs 文件中用到的服务容器 Services 就是该容器。

(2)第8行, _bootstrapHostBuilder = new BootstrapHostBuilder(Services, _hostBuilder.Properties); 创建了一个引导配置程序对象。

(3)第11行,执行 _bootstrapHostBuilder.ConfigureDefaults(args: null); 使用预先配置的默认值配置现有的 IHostBuilder 实例。

ConfigureDefaults()方法内部实现如下:

复制代码
 1 public static IHostBuilder ConfigureDefaults(this IHostBuilder builder, string[] args)
 2         {
 3             builder.UseContentRoot(Directory.GetCurrentDirectory());
 4             builder.ConfigureHostConfiguration(config =>
 5             {
 6                 config.AddEnvironmentVariables(prefix: "DOTNET_");
 7                 if (args is { Length: > 0 })
 8                 {
 9                     config.AddCommandLine(args);
10                 }
11             });
12 
13             builder.ConfigureAppConfiguration((hostingContext, config) =>
14             {
15                 IHostEnvironment env = hostingContext.HostingEnvironment;
16                 bool reloadOnChange = GetReloadConfigOnChangeValue(hostingContext);
17 
18                 config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: reloadOnChange)
19                         .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: reloadOnChange);
20 
21                 if (env.IsDevelopment() && env.ApplicationName is { Length: > 0 })
22                 {
23                     var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
24                     if (appAssembly is not null)
25                     {
26                         config.AddUserSecrets(appAssembly, optional: true, reloadOnChange: reloadOnChange);
27                     }
28                 }
29 
30                 config.AddEnvironmentVariables();
31 
32                 if (args is { Length: > 0 })
33                 {
34                     config.AddCommandLine(args);
35                 }
36             })
37             .ConfigureLogging((hostingContext, logging) =>
38             {
39                 bool isWindows =
40 #if NET6_0_OR_GREATER
41                     OperatingSystem.IsWindows();
42 #else
43                     RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
44 #endif
45 
46                 // IMPORTANT: This needs to be added *before* configuration is loaded, this lets
47                 // the defaults be overridden by the configuration.
48                 if (isWindows)
49                 {
50                     // Default the EventLogLoggerProvider to warning or above
51                     logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
52                 }
53 
54                 logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
55 #if NET6_0_OR_GREATER
56                 if (!OperatingSystem.IsBrowser())
57 #endif
58                 {
59                     logging.AddConsole();
60                 }
61                 logging.AddDebug();
62                 logging.AddEventSourceLogger();
63 
64                 if (isWindows)
65                 {
66                     // Add the EventLogLoggerProvider on windows machines
67                     logging.AddEventLog();
68                 }
69 
70                 logging.Configure(options =>
71                 {
72                     options.ActivityTrackingOptions =
73                         ActivityTrackingOptions.SpanId |
74                         ActivityTrackingOptions.TraceId |
75                         ActivityTrackingOptions.ParentId;
76                 });
77 
78             })
79             .UseDefaultServiceProvider((context, options) =>
80             {
81                 bool isDevelopment = context.HostingEnvironment.IsDevelopment();
82                 options.ValidateScopes = isDevelopment;
83                 options.ValidateOnBuild = isDevelopment;
84             });
85 
86             return builder;
87 
88             [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Calling IConfiguration.GetValue is safe when the T is bool.")]
89             static bool GetReloadConfigOnChangeValue(HostBuilderContext hostingContext) => hostingContext.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true);
90         }
View Code
复制代码

其内部逻辑实现了如下功能:

  • 将内容根路径设置为由 Directory.GetCurrentDirectory() 返回的路径
  • 通过以下对象加载主机配置
    • 前缀为 DOTNET_ 的环境变量
    • 命令行参数
  • 按以下顺序加载应用配置(优先级从低到高。如果出现同名配置,则优先级高的配置覆盖优先级低的配置)
    • appsettings.json 
    • appsettings.{EnvironmentName}.json 
    • 判断如果是开发者模式(Development),则加载应用程序文件(xx.dll)并读取用户秘钥信息
    • 环境变量
    • 命令行参数

在主机和应用程序配置中设置相同的配置键时,将使用应用程序的配置值。

  • 配置 Logging 日志信息
    • 如果应用程序是运行在Windows平台,则添加 EventLogLoggerProvider 过滤器,并设置日志级别。
    • 读取 appsettings.json 与 appsettings.{EnvironmentName}.json 文件中的 Logging节点配置的日志信息。

           

    • 如果当前应用程序不是在浏览器中作为WASM运行,则向日志工厂中添加 Console 控制台日志提供程序。
    • 向日志工厂中添加 Debug 日志提供程序。
    • 向日志工厂中添加 EventSource 日志提供程序。
    • 向日志工厂中添加 EventSourceLogger 日志提供程序。
    • 如果应用程序是运行在 Windows 平台,则向日志工厂中添加 EventLog 日志提供程序。
  • 配置默认的服务提供程序。当为“开发”环境时,启用范围验证依赖关系验证

(4)第26至33行,执行 ConfigureWebHostDefaults() 方法以配置Web主机默认信息,其中设置了应用程序的名称。

(5)第36至45行,执行 ConfigureHostConfiguration() 方法以配置主机默认信息(先读取命令行参数,然后再应用主机配置)。

(6)第47行,Configuration = new(); 创建一个 ConfigurationManager 对象

(7)第55行,执行 var (hostContext, hostConfiguration) = _bootstrapHostBuilder.RunDefaultCallbacks(Configuration, _hostBuilder); 主要应用程序配置。逻辑如下:

复制代码
 1         public (HostBuilderContext, ConfigurationManager) RunDefaultCallbacks(ConfigurationManager configuration, HostBuilder innerBuilder)
 2         {
 3             var hostConfiguration = new ConfigurationManager();
 4 
 5             foreach (var configureHostAction in _configureHostActions)
 6             {
 7                 configureHostAction(hostConfiguration);
 8             }
 9 
10             // This is the hosting environment based on configuration we've seen so far.
11             var hostingEnvironment = new HostingEnvironment()
12             {
13                 ApplicationName = hostConfiguration[HostDefaults.ApplicationKey],
14                 EnvironmentName = hostConfiguration[HostDefaults.EnvironmentKey] ?? Environments.Production,
15                 ContentRootPath = HostingPathResolver.ResolvePath(hostConfiguration[HostDefaults.ContentRootKey]),
16             };
17 
18             hostingEnvironment.ContentRootFileProvider = new PhysicalFileProvider(hostingEnvironment.ContentRootPath);
19 
20             // Normalize the content root setting for the path in configuration
21             hostConfiguration[HostDefaults.ContentRootKey] = hostingEnvironment.ContentRootPath;
22 
23             var hostContext = new HostBuilderContext(Properties)
24             {
25                 Configuration = hostConfiguration,
26                 HostingEnvironment = hostingEnvironment,
27             };
28 
29             // Split the host configuration and app configuration so that the
30             // subsequent callback don't get a chance to modify the host configuration.
31             configuration.SetBasePath(hostingEnvironment.ContentRootPath);
32 
33             // Chain the host configuration and app configuration together.
34             configuration.AddConfiguration(hostConfiguration, shouldDisposeConfiguration: true);
35 
36             // ConfigureAppConfiguration cannot modify the host configuration because doing so could
37             // change the environment, content root and application name which is not allowed at this stage.
38             foreach (var configureAppAction in _configureAppActions)
39             {
40                 configureAppAction(hostContext, configuration);
41             }
42 
43             // Update the host context, everything from here sees the final
44             // app configuration
45             hostContext.Configuration = configuration;
46 
47             foreach (var configureServicesAction in _configureServicesActions)
48             {
49                 configureServicesAction(hostContext, _services);
50             }
51 
52             foreach (var callback in _remainingOperations)
53             {
54                 callback(innerBuilder);
55             }
56 
57             return (hostContext, hostConfiguration);
58         }
View Code
复制代码

(8)第95至98行,设置了Environment、Logging、Host、WebHost等信息

(9)第72行,以上所有逻辑实现了web主机与应用程序的配置信息,最后将 Configuration 配置对象加入服务容器中,供开发者在应用程序中使用

2、进入到 WebApplicationBuilder 类的内部查看构造函数,其包含如下重要属性

通过它们可以设置与或获取配置信息、环境信息、日志信息、请求地址列表、服务集合等。其中最常用的是通过 Services 属性向容器中注册各种服务,配置的服务在整个应用程序中可用,供开发者在不同的业务逻辑中调用

3、执行 builder.Build() 方法,使用一组默认选项配置主机

  • 将 Kestrel 用作 Web 服务器并启用 IIS 集成。
  • 从 appsettings.json、环境变量、命令行参数和其他配置源中加载配置
  • 将日志记录输出发送到控制台并调试提供程序。

其内部实现逻辑如下:

复制代码
 1         public WebApplication Build()
 2         {
 3             // 在此连接主机配置。我们不会试图保留配置源代码本身,因为不支持在创建生成器后更改主机值
 4             _hostBuilder.ConfigureHostConfiguration(builder =>
 5             {
 6                 builder.AddInMemoryCollection(_hostConfigurationValues);
 7             });
 8 
 9             var chainedConfigSource = new TrackingChainedConfigurationSource(Configuration);
10 
11             // 通过将已构建的配置提供程序复制到最终的配置生成器来连接应用程序配置。将现有提供程序包装在配置源中,以避免重新构建已添加的配置源。
12             _hostBuilder.ConfigureAppConfiguration(builder =>
13             {
14                 builder.Add(chainedConfigSource);
15 
16                 foreach (var (key, value) in ((IConfigurationBuilder)Configuration).Properties)
17                 {
18                     builder.Properties[key] = value;
19                 }
20             });
21 
22             // 这需要转到此处以避免添加两次引导服务器的 IHostedService(GenericWebHostService)。
23             // 将通过 WebApplicationBuilder.services 添加的服务复制到最终的 IServiceCollection 中
24             _hostBuilder.ConfigureServices((context, services) =>
25             {
26                 /* 此时,只添加了由 GenericWebHostBuilder 和 WebHost.ConfigureWebDefaults 配置的服务。
27                  * HostBuilder 在 HostBuilder.Build()中发布了一个新的 ServiceCollection,现在还没有看到,因此无法清除这些服务,即使有些服务是多余的,
28                  * 因为在 _deferredHostBuilder 和 _hostBuilder 上调用 ConfigureWebHostDefaults。
29                  */
30                 foreach (var s in _services)
31                 {
32                     services.Add(s);
33                 }
34 
35                 /* 添加最初添加的托管服务,这将确保添加的任何托管服务都在初始托管服务集之后运行。这意味着托管服务在web主机启动之前运行。*/
36                 foreach (var s in _services.HostedServices)
37                 {
38                     services.Add(s);
39                 }
40 
41                 _services.HostedServices.Clear();
42 
43                 /* 将任何服务添加到用户可见的服务集合中,以便在用户捕获services属性时可以观察到这些服务。Orchard这样做是为了获得服务集合的“蓝图”*/
44 
45                 /* 删除对现有集合的引用,并将内部集合设置为新集合。这允许具有对服务集合的引用的代码仍然运行 */
46                 _services.InnerCollection = services;
47 
48                 /* 在 TrackingChainedConfigurationSource 之前添加的任何配置源(即_hostConfigurationValues中的主机配置),
49                  * 通过在开头使用 _hostConfigurationValue 插入配置值,避免覆盖通过 configuration 设置的配置值。
50                  */
51                 var beforeChainedConfig = true;
52                 var hostBuilderProviders = ((IConfigurationRoot)context.Configuration).Providers;
53 
54                 if (!hostBuilderProviders.Contains(chainedConfigSource.BuiltProvider))
55                 {
56                     /* 删除了指向ConfigurationManager的_hostBuilder的TrackingChainedConfigurationSource。这可能是使用WebApplicationFactory进行的测试。
57                      * 通过清除ConfirmationManager源复制效果。 
58                      */
59                     ((IConfigurationBuilder)Configuration).Sources.Clear();
60                     beforeChainedConfig = false;
61                 }
62 
63                 /* 使 ConfigurationManager 与最终 _hostBuilder 的配置匹配。
64                  * 为此,将额外的提供程序添加到 ConfigurationManager 的内部 _hostBuilders 配置中。
65                  * 将现有提供程序包装在配置源中,以避免重新生成或重新加载已添加的配置源。 
66                  */
67                 foreach (var provider in hostBuilderProviders)
68                 {
69                     if (ReferenceEquals(provider, chainedConfigSource.BuiltProvider))
70                     {
71                         beforeChainedConfig = false;
72                     }
73                     else
74                     {
75                         IConfigurationBuilder configBuilder = beforeChainedConfig ? _hostConfigurationManager : Configuration;
76                         configBuilder.Add(new ConfigurationProviderSource(provider));
77                     }
78                 }
79             });
80 
81             // 在最终的主机生成器上运行其他回调
82             Host.RunDeferredCallbacks(_hostBuilder);
83 
84             _builtApplication = new WebApplication(_hostBuilder.Build());
85 
86             //将服务集合标记为只读以防止将来修改
87             _services.IsReadOnly = true;
88 
89             /* 解析_hostBuilder的Configuration和builder.Configuration,以在服务提供程序中将两者标记为已解析,以确保两者都将与提供程序一起正确处理。 */
90             _ = _builtApplication.Services.GetService<IEnumerable<IConfiguration>>();
91 
92             return _builtApplication;
93         }
View Code
复制代码
总结

  ASP.NET Core 应用配置和启动“主机”。 主机负责应用程序启动和生存期管理。 ASP.NET Core 模板创建的 WebApplicationBuilder 包含主机。 虽然可以在主机和应用程序配置提供程序中完成一些配置,但通常,只有主机必需的配置才应在主机配置中完成。应用程序配置具有最高优先级

posted @   张传宁  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
页脚 HTML 代码
点击右上角即可分享
微信分享提示