【.Net技术栈梳理】10-.NET Core 程序的执行

发布于:2025-09-13 ⋅ 阅读:(20) ⋅ 点赞:(0)

理解 .NET Core 程序的执行顺序和中间件模型,是构建高效、可定制 Web 应用程序的关键。

这里将从程序的启动入口开始,详细讲解整个执行过程,并深入剖析中间件的加载和运作机制。

1. .NET Core 程序的执行顺序与运行过程

整个过程可以清晰地分为构建(Build)运行(Run) 两大阶段。

自 .NET 6 引入的“最小托管模型”使得这一过程更加简洁明了。

1.1 阶段 1:构建主机(配置和服务的准备)

一切始于 Program.cs 中的 WebApplication.CreateBuilder(args) 方法。

// Program.cs (.NET 6+)
var builder = WebApplication.CreateBuilder(args);

这行代码在背后做了大量工作:

  1. 初始化配置(Configuration):

    • 创建了一个 ConfigurationManager 对象。
    • 按照预定的顺序(后添加的源会覆盖先添加的)从各种配置源加载配置:
      • appsettings.json 和 appsettings.{Environment}.json(如 appsettings.Development.json)
      • 环境变量
      • 命令行参数
      • 用户机密(仅在开发环境)
    • 最终形成一个统一的配置根,可以通过 builder.Configuration 访问。
  2. 配置依赖注入(Dependency Injection, DI)容器:

    • 创建了一个 IServiceCollection 的实例。
    • 自动添加框架基础服务:如日志(ILogging)、配置(IConfiguration)、WebHost 环境(IWebHostEnvironment)等最核心的服务。
    • 此时,我们可以通过 builder.Services 来注册我们应用自己的服务(如 AddControllers, AddDbContext, AddScoped, AddSingleton 等)。
  3. 创建主机基础结构:

    • 配置 Kestrel Web 服务器(默认的、跨平台的高性能服务器)。
    • 配置日志记录提供程序。

此阶段的总结:WebApplicationBuilder 就像一个“总工程师”,它按照蓝图(各种配置源)准备好了所有原材料(配置)和工具(服务),并搭建好了工厂(主机)的基础设施。

1.2 阶段 2:运行主机(中间件管道的构建与请求处理)

接下来是 var app = builder.Build(); 和后续的配置。

var app = builder.Build();
  1. 构建服务容器:

    • builder.Build() 方法使用之前注册的所有服务(builder.Services)来构建最终的 IServiceProvider(即依赖注入容器)。
    • 此后,无法再注册新的服务。
  2. 配置中间件管道(Middleware Pipeline):

    • 这是最核心、最能体现执行顺序的部分。app 对象(WebApplication 类型)提供了配置请求管道的方法。
    • 管道是一个请求委托(Request Delegate) 的链表,每个委托都可以对传入的 HTTP 请求进行操作,然后选择将其传递给下一个委托,或者直接终止管道(短路)。
    • 中间件的添加顺序决定了它们的执行顺序
  3. 运行应用程序:

    • 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) 将请求传递给下一个中间件。

经典的模式: “环绕” 或 “洋葱” 模型

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>");
});

对于上述管道,请求/响应的流程和输出将是

Request -> First Middleware ("Hello") -> Terminal Middleware ("Handled") -> First Middleware ("Goodbye") -> Response

2.5 短路(Short-Circuiting)

中间件可以选择不调用 next(),从而直接终止管道,处理请求并返回响应。这称为“短路”。

  • 静态文件中间件:如果请求匹配到一个物理文件(如 site.css),它会直接返回该文件并短路管道。

  • 身份认证中间件:如果请求未认证且访问的是需要认证的资源,它可以重定向到登录页或返回 401 状态码。

  • 自定义中间件:例如,一个请求日志中间件发现 404 错误,可以直接返回一个自定义的 404 页面,而无需经过后续昂贵的 MVC 路由系统。

// 一个短路示例:健康检查端点
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 创建自定义中间件

方法一:约定式中间件类

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 接口

public class CustomMiddleware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // 前置逻辑
        await next(context); // 传递上下文
        // 后置逻辑
    }
}

// 注册(需要在DI容器中注册)
builder.Services.AddTransient<CustomMiddleware>();
app.UseMiddleware<CustomMiddleware>();

总结

  1. 执行顺序:.NET Core 程序启动遵循 构建配置与服务注册 -> 构建容器与中间件管道 -> 运行监听 的清晰流程。

  2. 运行过程:每个 HTTP 请求都被包装为 HttpContext,并流经预先构建好的中间件管道。

  3. 中间件本质:是处理 HttpContext 的委托链,通过 next 串联。

  4. 核心原则顺序至关重要。中间件的添加顺序决定了它们处理请求和响应的顺序,直接影响应用的行为、性能和安全性。

  5. 设计模式:采用“洋葱模型”,请求先逐层深入,响应再逐层返回。中间件有权决定是否传递请求(短路)。

理解了这个流程和中间件模型,就能非常灵活地定制 ASP.NET Core 应用程序的行为,例如添加全局异常处理、自定义认证、日志记录、性能监控等组件,并将它们精确地插入到管道的合适位置。


网站公告

今日签到

点亮在社区的每一天
去签到