ASP.NET Core Minimal API 底层原理:一个 HTTP 请求到底经历了什么?
前言
上一篇介绍了为什么我会在 STM32MP257 + Linux ARM64 设备上使用 ASP.NET Core,并采用:
ASP.NET Core Minimal API
+
Static HTML/CSS/JavaScript
+
BackgroundService
+
Native AOT
+
systemd
作为整个设备 Web 配置服务的基础架构。
这一篇不再停留在:
app.MapGet("/api/status", () => "OK");
这种使用层面。
我们真正要回答的是:
当浏览器请求
/api/status时,ASP.NET Core 内部到底发生了什么?
例如下面这段代码:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/api/status", () =>
{
return Results.Ok(new
{
status = "Connected"
});
});
app.Run();
看起来只有几行代码。
但实际上一个 HTTP 请求背后会经过:
TCP
↓
Kestrel
↓
HTTP Parser
↓
HttpContext
↓
Middleware Pipeline
↓
Routing
↓
Endpoint
↓
RequestDelegate
↓
Dependency Injection
↓
业务 Service
↓
Response Serialization
↓
HTTP Response
这一篇就把这个过程完整拆开。
一、先看整体架构
先建立一个整体概念。
ASP.NET Core 应用可以粗略理解为:
Client
│
│ HTTP
▼
┌─────────────┐
│ Kestrel │
└──────┬──────┘
│
▼
HttpContext
│
▼
Middleware Pipeline
│
▼
Routing
│
▼
Endpoint
│
▼
RequestDelegate
│
▼
Application Service
│
▼
Response
对于我们设备端项目,则可能是:
Browser
│
▼
Kestrel
│
▼
Minimal API
│
▼
FiveGStatusService
│
▼
Runtime Snapshot
│
▼
JSON
│
▼
Browser
这里非常关键的一点是:
ASP.NET Core 本质上不是“Controller 框架”。
Controller、Minimal API、Razor Pages 都只是建立在 ASP.NET Core HTTP Pipeline 上面的不同编程模型。
真正核心的是:
Host
+
Server
+
Middleware
+
Routing
+
Endpoint
+
Dependency Injection
二、WebApplication.CreateBuilder 到底干了什么
我们通常从下面一行开始:
var builder = WebApplication.CreateBuilder(args);
表面看起来只是创建了一个 builder。
实际上,它会初始化 ASP.NET Core 应用运行所需要的一整套基础设施。
可以粗略理解为:
WebApplication.CreateBuilder
│
├── Configuration
│
├── Logging
│
├── Dependency Injection
│
├── Host
│
├── Environment
│
└── Web Server
也就是说,它不是单纯创建一个 Web 对象。
它是在搭建整个应用的运行环境。
三、Generic Host 是什么
ASP.NET Core 应用底层依赖的是 .NET Generic Host。
Host 可以理解成:
应用程序的宿主。
它负责管理整个程序的生命周期。
例如:
Application Start
│
▼
Service initialization
│
▼
Web Server Start
│
▼
BackgroundService Start
│
▼
Application Running
│
▼
Shutdown Signal
│
▼
Service Stop
│
▼
Application Exit
所以 ASP.NET Core 并不只是:
HTTP Server
实际上更准确地说是:
Application Host
这也是为什么它特别适合设备端程序。
因为我们的设备程序不仅有 HTTP:
HTTP API
5G Service
Network Monitor
Configuration Service
Firmware Service
Background Worker
这些都可以挂在同一个 Host 里面。
例如:
builder.Services.AddHostedService<FiveGService>();
builder.Services.AddHostedService<NetworkMonitorService>();
启动 ASP.NET Core 时,这些后台服务也会一起启动。
四、builder.Services 是什么
我们经常会看到:
builder.Services.AddSingleton<DeviceStatusService>();
这里的:
builder.Services
实际上是:
IServiceCollection
它是 ASP.NET Core 依赖注入容器的注册表。
例如:
builder.Services.AddSingleton<DeviceStatusService>();
可以理解为注册了一条规则:
当有人需要 DeviceStatusService
↓
给他同一个 DeviceStatusService 实例
最终这些注册会在:
builder.Build();
时构建成:
IServiceProvider
也就是实际的依赖注入容器。
五、ASP.NET Core 的三种 Service 生命周期
ASP.NET Core 默认提供三种最常见生命周期。
Singleton
builder.Services.AddSingleton<MyService>();
整个应用生命周期只有一个实例:
Application
│
└── MyService #1
所有请求共享:
Request 1 ─┐
Request 2 ─┼── MyService #1
Request 3 ─┘
设备状态 Service 很多时候适合 Singleton。
例如:
FiveGRuntimeService
DeviceConfigurationService
NetworkStateService
因为它们维护的是:
设备全局状态。
Scoped
builder.Services.AddScoped<MyService>();
通常每个 HTTP Request 一个实例:
Request 1
└── MyService #1
Request 2
└── MyService #2
在 Web 后台、数据库应用中非常常见。
例如:
DbContext
经常采用 Scoped。
Transient
builder.Services.AddTransient<MyService>();
每次请求依赖时创建新实例:
Resolve
↓
new MyService()
Resolve
↓
new MyService()
适合:
轻量
无状态
短生命周期
对象。
六、builder.Build() 做了什么
接下来:
var app = builder.Build();
这一步非常重要。
之前:
builder
主要还处于“配置阶段”。
例如:
注册 Service
注册 Logging
读取 Configuration
设置 Host
调用 Build 后:
ServiceCollection
│
▼
ServiceProvider
Configuration
│
▼
Host
Web Server Configuration
│
▼
Application
最终得到:
WebApplication
可以粗略理解为:
WebApplication
=
Host
+
ServiceProvider
+
Middleware Pipeline Builder
+
Endpoint Routing
七、app.MapGet 到底做了什么
我们来看:
app.MapGet("/api/status", () =>
{
return Results.Ok("Connected");
});
很多人会以为这里已经启动了 HTTP 请求处理。
其实没有。
这一步本质上是在:
注册 Endpoint。
也就是告诉 ASP.NET Core:
如果:
HTTP Method = GET
Path = /api/status
那么:
执行这个 Handler
可以理解为建立了一张表:
| Method | Path | Handler |
|---|---|---|
| GET | /api/status | Status Handler |
| GET | /api/network | Network Handler |
| POST | /api/config | Config Handler |
Routing 后续就会根据请求匹配这里的 Endpoint。
八、什么是 Endpoint
Endpoint 是 ASP.NET Core 非常核心的概念。
一个 Endpoint 可以理解为:
一个可以处理请求的最终执行单元。
它通常包含:
Route Pattern
HTTP Method
Metadata
RequestDelegate
例如:
app.MapGet("/api/status", GetStatus);
内部概念大致是:
Endpoint
├── Route: /api/status
├── Method: GET
├── Metadata
└── RequestDelegate
最终 Routing 会找到它,然后执行。
九、RequestDelegate 是什么
ASP.NET Core HTTP Pipeline 的核心委托是:
RequestDelegate
其概念定义类似:
delegate Task RequestDelegate(HttpContext context);
也就是说,ASP.NET Core 最终把 HTTP 请求抽象成:
HttpContext
│
▼
RequestDelegate
│
▼
Task
所以从最底层来看:
ASP.NET Core 就是在不断调用一系列处理
HttpContext的 delegate。
十、HttpContext 是什么
这是 ASP.NET Core 请求处理中最重要的对象之一。
每一个 HTTP Request 都会创建一个:
HttpContext
里面包含:
HttpContext
│
├── Request
│
├── Response
│
├── User
│
├── Items
│
├── Connection
│
├── RequestServices
└── Features
例如:
app.MapGet("/debug", (HttpContext context) =>
{
return new
{
method = context.Request.Method,
path = context.Request.Path,
host = context.Request.Host.ToString()
};
});
如果请求:
GET /debug HTTP/1.1
Host: 192.168.0.10:8080
那么:
HttpContext.Request.Method
=
GET
HttpContext.Request.Path
=
/debug
十一、Kestrel 到底是什么
ASP.NET Core 默认使用:
Kestrel
作为 Web Server。
Kestrel 真正负责的是:
监听 Socket
建立 TCP Connection
解析 HTTP
创建 Request
发送 Response
所以:
Browser
并不是直接访问 Minimal API。
中间首先经过:
Browser
│
│ TCP
▼
Kestrel
Kestrel 收到数据以后:
GET /api/status HTTP/1.1
Host: 192.168.0.10:8080
Accept: application/json
它需要解析:
Method = GET
Path = /api/status
Headers
Body
Protocol
然后 ASP.NET Core 才能继续处理。
十二、Kestrel 和 ASP.NET Core 是什么关系
这两个概念容易混淆。
简单来说:
ASP.NET Core
=
Web Framework
而:
Kestrel
=
Web Server
关系大概是:
TCP
↓
Kestrel
↓
ASP.NET Core Pipeline
↓
Application
类似于传统架构里的:
nginx
↓
Application
只不过 Kestrel 已经内置在 ASP.NET Core 运行体系中。
因此对于我们的嵌入式设备:
Browser
↓
Kestrel
↓
ASP.NET Core
很多时候不需要额外部署 nginx。
十三、app.Run() 才真正启动应用
最后:
app.Run();
这里才真正进入:
Application Running
阶段。
主要过程可以理解为:
Host Start
│
├── Start Kestrel
│
├── Start IHostedService
│
├── Start BackgroundService
│
└── Listen for shutdown
例如我们设置:
ASPNETCORE_URLS=http://0.0.0.0:8080
那么 Kestrel 就开始监听:
0.0.0.0:8080
十四、一个 HTTP 请求正式开始
假设浏览器请求:
GET /api/status
第一步:
Browser
│
▼
TCP Connection
例如:
192.168.0.100:53024
↓
192.168.0.10:8080
Kestrel Accept 这个 TCP Connection。
然后读取 HTTP 数据。
十五、Kestrel 解析 HTTP Request
浏览器发送:
GET /api/status HTTP/1.1
Host: 192.168.0.10:8080
Accept: application/json
Connection: keep-alive
Kestrel 解析以后得到:
Method
=
GET
Path
=
/api/status
Protocol
=
HTTP/1.1
然后创建请求相关数据结构。
最终 ASP.NET Core 得到一个:
HttpContext
十六、请求进入 Middleware Pipeline
接下来请求进入:
Middleware Pipeline
Middleware 是 ASP.NET Core 最重要的架构之一。
例如:
app.UseMiddleware<A>();
app.UseMiddleware<B>();
app.UseMiddleware<C>();
请求路径:
Request
│
▼
Middleware A
│
▼
Middleware B
│
▼
Middleware C
│
▼
Endpoint
Response 返回:
Endpoint
│
▼
Middleware C
│
▼
Middleware B
│
▼
Middleware A
│
▼
Client
所以 Middleware 是一个典型的:
Chain of Responsibility
也就是责任链模式。
十七、Middleware 的核心结构
一个 Middleware 的概念代码大概如下:
public async Task InvokeAsync(
HttpContext context,
RequestDelegate next)
{
// 请求之前
await next(context);
// 响应之后
}
这里的:
next
代表下一个 Middleware。
因此:
await next(context);
实际上就是:
把请求继续向 Pipeline 后面传递。
十八、Middleware 为什么可以处理请求前后两个阶段
例如:
app.Use(async (context, next) =>
{
Console.WriteLine("Before");
await next();
Console.WriteLine("After");
});
执行过程:
Before
↓
下一个 Middleware
↓
Endpoint
↓
下一个 Middleware 返回
↓
After
这其实类似:
进入调用栈
↓
执行 Endpoint
↓
退出调用栈
所以 Middleware 很适合:
Logging
Authentication
Authorization
Exception Handling
CORS
Request Timing
Security Headers
十九、Routing 在什么时候发生
假设:
GET /api/status
进入 Pipeline。
Routing 会检查所有注册过的 Endpoint:
GET /api/status
GET /api/network
POST /api/config
然后匹配:
Method = GET
Path = /api/status
最终选择:
Status Endpoint
可以理解为:
HttpRequest
│
▼
Endpoint Routing
│
├── /api/status
├── /api/network
├── /api/config
└── ...
二十、Minimal API 参数是怎么自动注入的
来看:
app.MapGet(
"/api/status",
(DeviceStatusService service) =>
{
return service.GetStatus();
});
这里我们没有手动写:
app.Services.GetRequiredService<DeviceStatusService>();
但 ASP.NET Core 自动把 Service 注入了进来。
为什么?
因为 Minimal API 在构建 Endpoint 时,会分析 Handler 参数。
例如:
(DeviceStatusService service)
发现:
DeviceStatusService
可以从 Dependency Injection Container 获取。
于是请求执行时:
HttpContext
│
▼
RequestServices
│
▼
IServiceProvider
│
▼
DeviceStatusService
最终调用:
service.GetStatus();
二十一、Minimal API 参数绑定
Minimal API 还能自动绑定很多不同来源。
例如:
app.MapGet(
"/api/users/{id}",
(int id) =>
{
...
});
请求:
/api/users/10
ASP.NET Core 自动得到:
id = 10
数据来源可以包括:
Route
Query String
Header
Body
Form
Dependency Injection
例如:
app.MapGet(
"/api/device",
(
string name,
DeviceService service
) =>
{
...
});
请求:
/api/device?name=PLC
那么:
name
来自 Query String。
而:
service
来自 DI Container。
二十二、Endpoint Handler 真正执行
Routing 找到 Endpoint 后:
Endpoint
↓
RequestDelegate
↓
Handler
例如:
() =>
{
return Results.Ok(
new
{
status = "Connected"
});
}
最终返回:
IResult
二十三、Results.Ok 做了什么
代码:
Results.Ok(data)
并不是立即发送 HTTP Response。
它实际上返回一个:
IResult
可以理解为:
Ok Result
│
├── StatusCode = 200
└── Value = data
后面 ASP.NET Core 再执行这个 Result。
最终形成:
HTTP/1.1 200 OK
Content-Type: application/json
以及 JSON Body。
二十四、对象是怎么变成 JSON 的
例如:
return Results.Ok(new
{
status = "Connected"
});
这里返回的是:
C# Object
ASP.NET Core 最后需要转换成:
{
"status": "Connected"
}
一般使用:
System.Text.Json
所以完整过程实际上是:
C# Object
│
▼
System.Text.Json
│
▼
UTF-8 JSON
│
▼
Response Body
对于 Native AOT 项目,这一步尤其重要。
因为:
JSON Serialization 经常涉及运行时类型信息。
因此在 Native AOT 环境下,通常会进一步使用:
JsonSerializerContext
+
Source Generator
后面我们会专门写一篇。
二十五、Response 最终怎么发送回浏览器
最终 ASP.NET Core 得到:
StatusCode
Headers
Body
例如:
HTTP/1.1 200 OK
Content-Type: application/json
{"status":"Connected"}
然后交给 Kestrel。
Kestrel:
HTTP Response
│
▼
TCP
│
▼
Browser
浏览器 JavaScript:
const response = await fetch("/api/status");
const data = await response.json();
最终得到:
{
status: "Connected"
}
二十六、完整请求链
现在把整个过程串起来。
浏览器:
GET /api/status
首先:
Browser
建立 TCP Connection:
Browser
│
▼
Kestrel
Kestrel:
Parse HTTP
然后创建:
HttpContext
进入:
Middleware Pipeline
之后:
Routing
找到:
/api/status Endpoint
然后:
Dependency Injection
解析:
DeviceStatusService
执行:
Endpoint Handler
返回:
Results.Ok(...)
然后:
System.Text.Json
序列化。
最后:
Kestrel
发送 HTTP Response。
完整图:
Browser
│
│ GET /api/status
▼
TCP
│
▼
Kestrel
│
▼
HTTP Parser
│
▼
HttpContext
│
▼
Middleware
│
▼
Routing
│
▼
Endpoint
│
▼
Parameter Binding
│
├──── DI
│
▼
Handler
│
▼
Service
│
▼
IResult
│
▼
JSON Serialization
│
▼
HttpResponse
│
▼
Kestrel
│
▼
TCP
│
▼
Browser
二十七、放到我们的设备项目里看
在 STM32MP257 项目中,例如浏览器调用:
GET /api/fiveg/status
设计上不应该变成:
HTTP Request
│
▼
发送 AT+C5GREG?
│
▼
等待 Modem
│
▼
返回 HTTP
因为这样 HTTP Request 和硬件操作被强耦合了。
问题很多:
串口慢
AT Timeout
Modem Reset
设备掉线
并发 Request
HTTP Timeout
二十八、更合理的设备端架构
应该采用:
BackgroundService
│
▼
5G Runtime
│
▼
AT Channel
│
▼
Modem
后台持续维护:
Runtime Snapshot
而 Web:
GET /api/fiveg/status
│
▼
读取 Snapshot
│
▼
返回 JSON
整个过程:
┌──────────────┐
│ Modem │
└──────▲───────┘
│ AT
│
┌──────┴───────┐
│ FiveGService │
└──────┬───────┘
│
▼
Runtime Snapshot
▲
│
Browser → API → StatusService
这样 HTTP Request 完全不需要等待硬件。
这是工业设备 Web 架构中非常重要的一点。
二十九、为什么不要在 API Handler 里直接做耗时硬件操作
假设:
app.MapGet(
"/api/status",
async () =>
{
var result =
await modem.SendAsync("AT+C5GREG?");
return Results.Ok(result);
});
看起来没问题。
但实际上存在:
HTTP Request
│
▼
等待 Serial Port
│
▼
等待 Modem
│
▼
AT Timeout
如果 modem 需要:
5 秒
整个 HTTP 请求就被占用 5 秒。
如果多个浏览器同时请求:
Request 1
Request 2
Request 3
Request 4
所有请求都可能去抢:
AT Channel
最终需要:
Command Queue
Lock
Timeout
Cancellation
Concurrency Control
复杂度迅速上升。
三十、Snapshot 模式
设备端状态查询比较推荐:
Producer
│
▼
Snapshot
▲
│
Consumer
例如:
FiveGService
│
│ 更新
▼
FiveGStatusSnapshot
▲
│ 读取
│
Web API
API:
app.MapGet(
"/api/fiveg/status",
(FiveGStatusService service) =>
{
return Results.Ok(
service.GetSnapshot());
});
请求执行时间就非常短:
读取内存
↓
JSON Serialize
↓
返回
而不是:
发 AT
↓
等待串口
↓
等待 Modem
↓
Parse
↓
返回
三十一、这也是 ASP.NET Core 和后台 Service 的边界
可以把职责划分为:
ASP.NET Core HTTP Layer
负责:
Request
Routing
Validation
Response
而:
Device Service Layer
负责:
Hardware
State Machine
Recovery
Polling
Timeout
Retry
这样形成:
HTTP
↓
Application Service
↓
Runtime Service
↓
Hardware
这个边界非常重要。
三十二、Minimal API 并不等于“没有架构”
很多人看到 Minimal API:
app.MapGet(...)
容易产生一种印象:
Minimal API 就是把所有代码写进 Program.cs。
这是错误的。
Minimal 的意思主要是:
Web API 编程模型更加轻量。
而不是:
应用架构也要最小化。
对于工业项目,我仍然建议:
Program.cs
│
├── Dependency Injection
├── Middleware
└── Endpoint Registration
真正业务逻辑:
Services
Domain
Infrastructure
分别维护。
例如:
Program.cs
│
▼
Api
│
▼
Services
│
▼
Infrastructure
三十三、推荐结构
例如:
Stm32ConfigWeb
│
├── Program.cs
│
├── Api
│ ├── FiveGEndpoints.cs
│ ├── NetworkEndpoints.cs
│ └── SystemEndpoints.cs
│
├── Services
│ ├── FiveG
│ ├── Network
│ └── Configuration
│
├── Models
│
├── Infrastructure
│ ├── Linux
│ ├── Serial
│ └── Storage
│
└── wwwroot
Endpoint 文件:
public static class FiveGEndpoints
{
public static void MapFiveGEndpoints(
this WebApplication app)
{
app.MapGet(
"/api/fiveg/status",
GetStatus);
}
private static IResult GetStatus(
FiveGStatusService service)
{
return Results.Ok(
service.GetSnapshot());
}
}
然后 Program.cs:
var builder =
WebApplication.CreateBuilder(args);
builder.Services
.AddSingleton<FiveGStatusService>();
var app = builder.Build();
app.MapFiveGEndpoints();
app.Run();
这样 Minimal API 仍然可以保持良好的工程结构。
三十四、从源码思想看 ASP.NET Core
如果把大量实现细节拿掉,ASP.NET Core 核心其实没有想象中复杂。
可以抽象成:
Server
│
▼
HttpContext
│
▼
RequestDelegate
│
▼
Middleware Chain
│
▼
Endpoint
│
▼
Application Code
最终最核心的思想就是:
对每一个 HttpContext 执行一条 RequestDelegate 链。
Minimal API、MVC、Razor Pages 都建立在这个基础之上。
三十五、一个极简版“ASP.NET Core”
如果自己模拟,可以想象有:
public delegate Task RequestDelegate(
HttpContext context);
然后:
RequestDelegate endpoint =
async context =>
{
await context.Response
.WriteAsync("Hello");
};
Middleware:
RequestDelegate pipeline =
async context =>
{
Console.WriteLine("Before");
await endpoint(context);
Console.WriteLine("After");
};
Server 收到 Request:
await pipeline(context);
这基本就是 ASP.NET Core Pipeline 最核心的抽象。
真正框架当然远比这个复杂,但思想就是如此。
三十六、性能为什么不错
ASP.NET Core 性能比较好的一个重要原因是:
整个请求链条相对直接
尤其 Minimal API:
Kestrel
↓
Routing
↓
Endpoint
↓
Handler
相比很多传统大型 Web Framework:
大量动态扫描
反射
复杂 Controller 生命周期
多层 Filter
整体执行路径比较轻量。
同时大量基础设施围绕:
async/await
ValueTask
Span<T>
Memory<T>
ArrayPool
Pipeline
进行性能优化。
三十七、嵌入式设备为什么更应该理解这些原理
普通企业 Web 项目:
32 GB RAM
16 Core CPU
Kubernetes
Cloud
很多性能浪费可能感知不到。
但设备端可能只有:
ARM Cortex-A
有限 RAM
有限 Flash
有限 CPU
因此你必须知道:
哪些对象长期存在
哪些操作会分配内存
哪些请求会阻塞
哪些 Service 是 Singleton
哪些逻辑运行在 BackgroundService
哪些数据只应该读取 Snapshot
这也是为什么在嵌入式设备上使用 ASP.NET Core 时:
理解框架底层原理,比单纯会写 API 更重要。
三十八、最终总结
回到最开始的问题:
当浏览器访问:
GET /api/status
ASP.NET Core 到底发生了什么?
完整过程是:
1. Browser 建立 TCP 连接
2. Kestrel 接收连接
3. Kestrel 解析 HTTP Request
4. ASP.NET Core 创建 HttpContext
5. Request 进入 Middleware Pipeline
6. Routing 根据 Path + Method 匹配 Endpoint
7. Minimal API 进行参数绑定
8. Dependency Injection 提供 Service
9. Endpoint Handler 执行业务逻辑
10. Handler 返回 IResult
11. System.Text.Json 序列化对象
12. ASP.NET Core 构建 HttpResponse
13. Kestrel 将 Response 写入 TCP
14. Browser 收到 JSON
因此我们平时写的:
app.MapGet(
"/api/status",
() => Results.Ok("Connected"));
虽然只有几行代码,但背后实际上是:
Host
+
Kestrel
+
HttpContext
+
Middleware
+
Routing
+
Endpoint
+
Dependency Injection
+
Serialization
共同完成的。
理解这一层以后,再看 ASP.NET Core 就不会只是:
MapGet
MapPost
Controller
而会真正理解:
ASP.NET Core 是一个围绕 HTTP Pipeline 构建的应用宿主框架。
对于我们这样的 Linux ARM64 工业设备项目,这一点尤其重要。
因为真正运行在 ASP.NET Core Host 中的不只是 Web API,还有:
配置服务
设备状态服务
5G 服务
后台轮询
状态机
恢复逻辑
Linux 系统访问
Web API 只是它对外暴露的一层接口。
下一篇
下一篇准备继续深入:
《ASP.NET Core BackgroundService 深度解析:为什么设备后台任务不能写在 HTTP API 里》
重点会讲:
IHostedService
BackgroundService
ExecuteAsync
CancellationToken
应用生命周期
异常传播
后台任务退出
线程池
async/await
定时轮询
设备状态机
并结合 Linux ARM64 设备上的 5G Modem 状态轮询,分析一种比较合理的工业设备后台任务架构。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/wojiuguowei/article/details/165474711



