1.2 搭建环境并追踪第一个请求 本节动手为主:装好 .NET 8 SDK,用命令行创建第一个 Web 项目,然后给项目加上请求日志中间件,亲眼观察一个 HTTP 请求进出管线的完整轨迹——为第 2 章拆解管线做好实验台。 上一节理清了年代线,这一节把观察哨架起来。我特意不用 IDE 的图形化新建项目向导:向导把十几步操作压成一个按钮,你在结果里看不到任何过程。命令行 + 最小程序结构,才看得见请求的流动。 安装 SDK 与验证 到微软官网下载 .NET 8 SDK 安装包(Windows 用安装器,macOS/Linux 有官方脚本),装完在命令行验证: SDK 里带着运行时、编译器、项目模板和本地 Web 服务器,一个命令全齐,不需要单独装 IIS 或别的服务器——这是 ASP.
本节动手为主:装好 .NET 8 SDK,用命令行创建第一个 Web 项目,然后给项目加上请求日志中间件,亲眼观察一个 HTTP 请求进出管线的完整轨迹——为第 2 章拆解管线做好实验台。
上一节理清了年代线,这一节把观察哨架起来。我特意不用 IDE 的图形化新建项目向导:向导把十几步操作压成一个按钮,你在结果里看不到任何过程。命令行 + 最小程序结构,才看得见请求的流动。
到微软官网下载 .NET 8 SDK 安装包(Windows 用安装器,macOS/Linux 有官方脚本),装完在命令行验证:
# 验证 SDK 已安装,8.0.x 即为目标版本 dotnet --version # 输出示例:8.0.404 # 查看本机所有已装的运行时与 SDK dotnet --list-sdks # 输出示例: # 8.0.404 [C:\Program Files\dotnet\sdk]
SDK 里带着运行时、编译器、项目模板和本地 Web 服务器,一个命令全齐,不需要单独装 IIS 或别的服务器——这是 ASP.NET 时代做不到的事。
选一个空目录,执行:
# 从模板创建空的 Web 项目,命名为 ObsStation(观察哨) dotnet new web -n ObsStation cd ObsStation dotnet run # 关键输出(留意端口,每次可能不同): # info: Microsoft.Hosting.Lifetime[14] # Now listening on: http://localhost:5173
浏览器访问那个地址,看到 "Hello World!" 就说明管线已经通了。按 Ctrl+C 停止。现在解剖这个项目的结构,为后面的观察做准备:
| 位置 | 作用 | 后续哪章展开 |
|---|---|---|
| Program 程序文件 | 入口:建主机、注册服务、编排中间件、启动监听 | 第 2 章 |
| Properties 组内的启动配置 | 开发环境的端口与 profile 设置 | 第 8 章环境部分 |
| appsettings 配置文件 | 分层配置的默认来源之一 | 第 2 章配置节 |
| 依赖包清单文件 | NuGet 依赖声明,框架本身也是一组包 | 第 2 章 |
打开入口文件,完整内容只有几行:
// 入口文件的全部内容——一个最小可运行的 ASP.NET Core 应用 var builder = WebApplication.CreateBuilder(args); // 1. 准备主机:配置、日志、容器都已就位 var app = builder.Build(); // 2. 组装应用 app.MapGet("/", () => "Hello World!"); // 3. 在管线上登记一个端点 app.Run(); // 4. 启动监听,阻塞直到进程退出
四行代码背后是一整套主机启动流程——这四行的展开正是第 2 章的主菜。现在先让它跑起来做实验。
光看到 Hello World 不过瘾。加一段中间件代码,记录每个请求的方法、路径、状态码与耗时:
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); // 观察哨中间件:注册在所有端点之前,因此能观察到每个请求 app.Use(async (ctx, next) => { var sw = System.Diagnostics.Stopwatch.StartNew(); await next(); // 请求继续流向管线后续各站 sw.Stop(); // 出站时输出:方法 路径 状态码 耗时 Console.WriteLine($"[哨站] {ctx.Request.Method} {ctx.Request.Path} " + $"-> {ctx.Response.StatusCode} ({sw.ElapsedMilliseconds}ms)"); }); app.MapGet("/", () => "Hello World!"); app.MapGet("/ping/{name}", (string name) => $"pong:{name}"); app.Run();
再次运行并访问几个地址,控制台输出:
[哨站] GET / -> 200 (1ms) [哨站] GET /ping/aspnet -> 200 (0ms) [哨站] GET /nope -> 404 (0ms)
三条输出值得逐条解读。第一条:请求命中根路径端点,状态码 200。第二条:路径参数 aspnet 被端点模板捕获并回显,这就是路由的最原始形态,第 3 章会展开成完整的路由系统。第三条最有意思——没有匹配端点时管线仍然完整走了一遍,哨站照样记录到 404,说明"没找到终点站"不等于"没进管线"。

💡 关键直觉:中间件的注册顺序就是执行顺序。观察哨放在端点登记之前,所以既能看到"进站"也能看到"出站"。如果把它放在后面,404 那条日志就会消失——这个小实验预告了第 2 章的核心规则。
开发期的体验优化:运行中改代码,另开终端执行:
# 热重载:不重启进程应用代码改动,观察哨的计数器等状态得以保留 dotnet watch # 输出示例: # info: Microsoft.DotNet.Watcher[0] # File changed: Program.cs # info: Microsoft.DotNet.Watcher[0] # Applying updates...
日常循环就是 dotnet watch + 浏览器刷新。需要彻底重启(改了启动配置或包引用)时再 Ctrl+C。
⚠️ 常见坑:改了端口配置但浏览器一直访问旧地址,以为是代码坏了。终端里 "Now listening on" 那一行永远以它为准,别信记忆。
run 是"编译—启动"一次到位;watch 是常驻进程,盯着文件变化做热重载,日常开发首选。但热重载有边界:改中间件内部逻辑通常能原地生效;改了服务注册、包引用或启动配置,它会自动降级为重启进程——看到终端提示重启属于正常行为,不是故障。
框架自带的控制台日志提供程序在输出,分类与级别这套机制 2.3 节展开。此刻只需要知道:这些刷屏的行里,"Now listening on" 与请求记录最有观察价值,其余可以暂时当作背景噪声。