文章目录
- [1. .NET Core 程序的执行顺序与运行过程](#1. .NET Core 程序的执行顺序与运行过程)
-
- [1.1 阶段 1:构建主机(配置和服务的准备)](#1.1 阶段 1:构建主机(配置和服务的准备))
- [1.2 阶段 2:运行主机(中间件管道的构建与请求处理)](#1.2 阶段 2:运行主机(中间件管道的构建与请求处理))
- [2. 中间件的加载与运作机制](#2. 中间件的加载与运作机制)
-
- [2.1 什么是中间件?](#2.1 什么是中间件?)
- [2.2 如何添加和配置中间件?](#2.2 如何添加和配置中间件?)
- [2.3 中间件的标准顺序("官方配方")](#2.3 中间件的标准顺序(“官方配方”))
- [2.4 中间件的运作模式:Request Delegate 和 next](#2.4 中间件的运作模式:Request Delegate 和 next)
- [2.5 短路(Short-Circuiting)](#2.5 短路(Short-Circuiting))
- [2.6 创建自定义中间件](#2.6 创建自定义中间件)
理解 .NET Core 程序的执行顺序和中间件模型,是构建高效、可定制 Web 应用程序的关键。
这里将从程序的启动入口开始,详细讲解整个执行过程,并深入剖析中间件的加载和运作机制。
1. .NET Core 程序的执行顺序与运行过程
整个过程可以清晰地分为构建(Build) 和运行(Run) 两大阶段。
自 .NET 6 引入的"最小托管模型"使得这一过程更加简洁明了。
1.1 阶段 1:构建主机(配置和服务的准备)
一切始于 Program.cs 中的 WebApplication.CreateBuilder(args) 方法。
csharp
// Program.cs (.NET 6+)
var builder = WebApplication.CreateBuilder(args);
这行代码在背后做了大量工作:
-
初始化配置(Configuration):
- 创建了一个 ConfigurationManager 对象。
- 按照预定的顺序(后添加的源会覆盖先添加的)从各种配置源加载配置:
- appsettings.json 和 appsettings.{Environment}.json(如 appsettings.Development.json)
- 环境变量
- 命令行参数
- 用户机密(仅在开发环境)
- 最终形成一个统一的配置根,可以通过 builder.Configuration 访问。
-
配置依赖注入(Dependency Injection, DI)容器:
- 创建了一个 IServiceCollection 的实例。
- 自动添加框架基础服务:如日志(ILogging)、配置(IConfiguration)、WebHost 环境(IWebHostEnvironment)等最核心的服务。
- 此时,我们可以通过 builder.Services 来注册我们应用自己的服务(如 AddControllers, AddDbContext, AddScoped, AddSingleton 等)。
-
创建主机基础结构:
- 配置 Kestrel Web 服务器(默认的、跨平台的高性能服务器)。
- 配置日志记录提供程序。
此阶段的总结:WebApplicationBuilder 就像一个"总工程师",它按照蓝图(各种配置源)准备好了所有原材料(配置)和工具(服务),并搭建好了工厂(主机)的基础设施。
1.2 阶段 2:运行主机(中间件管道的构建与请求处理)
接下来是 var app = builder.Build(); 和后续的配置。
csharp
var app = builder.Build();
-
构建服务容器:
- builder.Build() 方法使用之前注册的所有服务(builder.Services)来构建最终的 IServiceProvider(即依赖注入容器)。
- 此后,无法再注册新的服务。
-
配置中间件管道(Middleware Pipeline):
- 这是最核心、最能体现执行顺序的部分。app 对象(WebApplication 类型)提供了配置请求管道的方法。
- 管道是一个请求委托(Request Delegate) 的链表,每个委托都可以对传入的 HTTP 请求进行操作,然后选择将其传递给下一个委托,或者直接终止管道(短路)。
- 中间件的添加顺序决定了它们的执行顺序。
-
运行应用程序:
- app.Run(); 启动应用程序,开始监听配置的 URL(如 http://localhost:5000 或 https://localhost:7001)。
- Kestrel 开始接收传入的 HTTP 请求。
- 对于每个请求,Kestrel 会将其包装成一个 HttpContext 对象(包含了 HttpRequest 和 HttpResponse),然后将这个上下文对象送入中间件管道进行处理。
2. 中间件的加载与运作机制
2.1 什么是中间件?
中间件是组装成应用程序管道来处理请求和响应的软件组件。每个中间件组件:
- 选择是否将请求传递给管道中的下一个组件。
- 可以在调用下一个组件之前和之后执行工作。
2.2 如何添加和配置中间件?
在 app.Build() 之后,我们使用 WebApplication 上的方法来配置管道:
-
UseMiddleware< T >() / Use(...): 添加一个自定义的中间件类或内联中间件。
-
UseRouting(): 添加路由中间件,负责将请求匹配到端点(Endpoint)。
-
UseAuthentication(): 添加认证中间件。
-
UseAuthorization(): 添加授权中间件。
-
UseEndpoints(...): 添加端点中间件,用于执行匹配到的端点(如 MapControllers, MapRazorPages)。
-
Run(...): 添加一个终止中间件(管道末端,不会调用 next)。
-
Map(...): 创建管道分支(基于路径匹配)。
2.3 中间件的标准顺序("官方配方")
一个典型的、功能完整的中间件管道顺序如下,其结构可以通过以下流程图清晰展示:
HTTP Request 进入 异常/错误处理中间件
(UseExceptionHandler/UseDeveloperExceptionPage) HTTPS 重定向中间件
(UseHttpsRedirection) 静态文件中间件
(UseStaticFiles) 路由中间件
(UseRouting) 认证中间件
(UseAuthentication) 授权中间件
(UseAuthorization) 端点中间件
(UseEndpoints) 终端中间件
(Run) 返回响应
为什么顺序如此重要?
-
异常处理必须在最外层,以捕获管道中后续任何地方抛出的异常。
-
静态文件放在路由之前,因为对于像 css、js、image 这样的文件请求,不需要经过认证、授权等复杂逻辑,直接返回即可,性能最高。如果先进了路由,就找不到对应的 Controller 和 Action 了。
-
认证/授权 必须在路由之后、端点之前。因为路由中间件已经确定了请求要访问哪个端点(Endpoint),而授权策略([Authorize] 特性)是附加在端点(Controller/Action)上的。授权中间件需要知道目标端点是什么,才能决定应用哪种授权策略。
2.4 中间件的运作模式:Request Delegate 和 next
每个中间件本质上都是一个委托,其签名是 Task RequestDelegate(HttpContext context)。
管道中的每个中间件都可以通过调用 next(context) 将请求传递给下一个中间件。
经典的模式: "环绕" 或 "洋葱" 模型
csharp
app.Use(async (context, next) =>
{
// 1. 在调用下一个中间件之前执行的逻辑 (传入请求)
Log.Information("Request starting...");
await context.Response.WriteAsync("First Middleware Says Hello!<br>");
await next.Invoke(); // 将请求传递给管道中的下一个中间件
// 2. 在下一个中间件执行完毕回来后执行的逻辑 (传出响应)
Log.Information("Request finished.");
await context.Response.WriteAsync("First Middleware Says Goodbye!<br>");
});
app.Run(async (context) =>
{
await context.Response.WriteAsync("Terminal Middleware Handled the Request!<br>");
});
对于上述管道,请求/响应的流程和输出将是
csharp
Request -> First Middleware (写 "Hello") -> Terminal Middleware (写 "Handled") -> First Middleware (写 "Goodbye") -> Response
2.5 短路(Short-Circuiting)
中间件可以选择不调用 next(),从而直接终止管道,处理请求并返回响应。这称为"短路"。
-
静态文件中间件:如果请求匹配到一个物理文件(如 site.css),它会直接返回该文件并短路管道。
-
身份认证中间件:如果请求未认证且访问的是需要认证的资源,它可以重定向到登录页或返回 401 状态码。
-
自定义中间件:例如,一个请求日志中间件发现 404 错误,可以直接返回一个自定义的 404 页面,而无需经过后续昂贵的 MVC 路由系统。
csharp
// 一个短路示例:健康检查端点
app.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/health"))
{
context.Response.StatusCode = 200;
await context.Response.WriteAsync("Healthy");
return; // 短路,不调用 next
}
await next();
});
2.6 创建自定义中间件
方法一:约定式中间件类
csharp
public class RequestLoggerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
// 约定:必须包含RequestDelegate参数和可选的后续参数
public RequestLoggerMiddleware(RequestDelegate next, ILogger<RequestLoggerMiddleware> logger)
{
_next = next;
_logger = logger;
}
// 约定:必须叫Invoke或InvokeAsync,接收HttpContext参数
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation("Handling request: " + context.Request.Path);
await _next(context); // 调用管道中的下一个组件
_logger.LogInformation("Finished handling request.");
}
}
// 扩展方法,用于优雅注册
public static class RequestLoggerMiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogger(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggerMiddleware>();
}
}
// 在Program.cs中使用
app.UseRequestLogger(); // 非常简洁
方法二:实现 IMiddleware 接口
csharp
public class CustomMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// 前置逻辑
await next(context); // 传递上下文
// 后置逻辑
}
}
// 注册(需要在DI容器中注册)
builder.Services.AddTransient<CustomMiddleware>();
app.UseMiddleware<CustomMiddleware>();
总结
-
执行顺序 :.NET Core 程序启动遵循 构建配置与服务注册 -> 构建容器与中间件管道 -> 运行监听 的清晰流程。
-
运行过程:每个 HTTP 请求都被包装为 HttpContext,并流经预先构建好的中间件管道。
-
中间件本质:是处理 HttpContext 的委托链,通过 next 串联。
-
核心原则 :顺序至关重要。中间件的添加顺序决定了它们处理请求和响应的顺序,直接影响应用的行为、性能和安全性。
-
设计模式:采用"洋葱模型",请求先逐层深入,响应再逐层返回。中间件有权决定是否传递请求(短路)。
理解了这个流程和中间件模型,就能非常灵活地定制 ASP.NET Core 应用程序的行为,例如添加全局异常处理、自定义认证、日志记录、性能监控等组件,并将它们精确地插入到管道的合适位置。