2. 项目结构与启动机制
在 ASP.NET Core 的工程实践中,启动机制是理解整个应用生命周期的入口。早期模板把启动逻辑拆成 Program.cs 与 Startup.cs 两个文件:前者负责创建主机,后者通过 ConfigureServices 注册服务、通过 Configure 构建中间件管道。从 .NET 6 开始,模板默认采用“最小主机”模型,把这两部分合并进 Program.cs,用 WebApplicationBuilder 与 WebApplication 两个核心对象完成同样的工作。理解这两代写法的对应关系,是读懂现代 ASP.NET Core 项目结构的关键。
从 Startup 到最小主机:两代写法的对应关系
传统写法中,Program.cs 的 CreateHostBuilder 返回一个 IHostBuilder,由运行时反射调用 Startup 类。服务注册发生在 ConfigureServices(IServiceCollection services),管道装配发生在 Configure(IApplicationBuilder app, IWebHostEnvironment env)。最小主机写法把这两步变成 builder.Services 与 app 上的直接调用:
var builder = WebApplication.CreateBuilder(args);
// 对应 ConfigureServices:注册服务
builder.Services.AddControllers();
builder.Services.AddScoped<IOrderService, OrderService>();
var app = builder.Build();
// 对应 Configure:构建中间件管道
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.Run();
需要明确的是,最小主机并没有取消 Startup,它只是把默认入口简化了。如果团队已有大量基于 Startup 的代码,仍可用 builder.Host.ConfigureWebHostDefaults 或 WebApplication.CreateBuilder 配合 UseStartup<Startup>() 保留旧结构。选择哪种写法取决于迁移成本与团队约定,而不是功能差异。
WebApplicationBuilder 与 WebApplication 的职责边界
WebApplicationBuilder 是“构建期”对象,它暴露 Services、Configuration、Logging、Environment、Host 等属性。服务注册、配置源追加、日志提供程序设置都应在 builder.Build() 之前完成,因为一旦调用 Build(),服务容器就会被冻结,之后再向 Services 添加服务不会生效。WebApplication 是“运行期”对象,它同时实现 IHost 与 IApplicationBuilder,因此既能用 app.UseXxx() 装配中间件,也能用 app.Run() 启动主机、用 app.Urls 查看实际监听地址。
builder.Services 的注册写到 app 创建之后。此时容器已构建,新增注册不会进入运行时解析链,表现为“服务已注册却注入失败”。中间件管道的构建顺序
中间件按注册顺序形成一条“请求进入、响应返回”的双向链。请求先经过最先注册的中间件,响应则按相反方向返回。因此顺序错误会导致功能异常,而不是简单报错。典型顺序是:异常处理 → HTTPS 重定向 → 静态文件 → 路由 → 认证 → 授权 → 终结点。若把 UseAuthorization 放在 UseRouting 之前,授权中间件拿不到路由匹配出的终结点元数据,授权策略可能不生效。
环境变量与配置加载优先级
ASP.NET Core 的配置系统是“多源叠加”的:后加入的源会覆盖先加入的同名键。默认顺序大致为 appsettings.json、appsettings.{Environment}.json、用户机密(仅开发环境)、环境变量、命令行参数。环境变量使用双下划线表示层级,例如 ConnectionStrings__Default 对应配置中的 ConnectionStrings:Default。这意味着生产环境可以用环境变量覆盖镜像内的默认配置,而不必修改文件。
环境本身由 ASPNETCORE_ENVIRONMENT 决定,常见取值为 Development、Staging、Production。它会影响 appsettings.{Environment}.json 的加载、IsDevelopment() 的判断,以及是否启用开发者异常页。若该变量未设置,默认按 Production 处理,这一点在容器化部署时尤其容易踩坑。
launchSettings.json 与 Kestrel 启动参数
launchSettings.json 只影响本地开发,不参与发布产物。它定义多个 profile,每个 profile 可设置 applicationUrl、environmentVariables、commandName(如 Project、IIS Express)等。Visual Studio 或 dotnet run 会读取它来设置环境变量与监听地址。真正决定生产监听行为的是 Kestrel 配置,可写在 appsettings.json 的 Kestrel 节点,也可用 ASPNETCORE_URLS 或 --urls 命令行参数覆盖。
{
"profiles": {
"http": {
"commandName": "Project",
"applicationUrl": "http://localhost:5080",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"applicationUrl": "https://localhost:7080;http://localhost:5080",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
对应的 Kestrel 端点配置示例:
{
"Kestrel": {
"Endpoints": {
"Http": { "Url": "http://0.0.0.0:5080" },
"Https": {
"Url": "https://0.0.0.0:7080",
"Certificate": { "Path": "cert.pfx", "Password": "dev-only" }
}
}
}
}
dev-only 仅用于本地演示,生产环境应使用环境变量、密钥管理服务或证书存储。一次可复现的启动验证
要确认配置优先级与监听地址是否符合预期,可按以下方式验证:在项目根目录执行 dotnet run --urls "http://localhost:6000",观察控制台输出的 Now listening on: 行;随后在 appsettings.Development.json 中把某个自定义键设为 A,再用 set ConnectionStrings__Default=B(Windows)或 export ConnectionStrings__Default=B(Linux/macOS)设置环境变量,在最小 API 中读取 builder.Configuration["ConnectionStrings:Default"] 并输出,应得到 B,说明环境变量覆盖了 JSON 文件。若把该键同时写入命令行参数,则命令行值最终生效。通过这种“逐层覆盖”的验证,可以建立对配置优先级的稳定直觉,而不是依赖记忆。
把上述对象与顺序串起来看:WebApplicationBuilder 负责在构建期收集服务、配置与日志;WebApplication 负责在运行期按注册顺序装配管道并启动 Kestrel;launchSettings.json 只服务本地开发,环境变量与命令行参数则在部署时提供最终覆盖能力。掌握这条主线,后续阅读任何 ASP.NET Core 项目的 Program.cs 都能快速定位服务注册、管道顺序与监听配置的位置。