ASP.NET Core 学习笔记(一):基础架构与启动流程

写在前面

本文是 ASP.NET Core 学习笔记系列的第一篇。ASP.NET Core 是 .NET 后端的核心框架,但它和老的 ASP.NET(Framework)差别极大——完全重写、跨平台、高性能、模块化。很多人用着用着会觉得"能跑但不清楚为什么这么设计"。

本系列从架构层面讲透 ASP.NET Core。第一篇聚焦最核心的两件事:中间件管道依赖注入——理解了这两个,整个框架就通了。

版本说明:基于 .NET 8 LTS。从 .NET 6 起统一用 Program.cs(top-level statements),不再有 Startup.cs


一、ASP.NET Core 是什么

1.1 定义

ASP.NET Core 是微软的开源、跨平台、高性能 Web 框架,用来构建 Web 应用、API、微服务、实时应用(SignalR)等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
它能构建:
  - Web API(RESTful 后端,最常见)
  - Web 应用(MVC、Razor Pages、Blazor)
  - 实时应用(SignalR、WebSocket)
  - 后台服务(Worker Service)
  - gRPC 服务
  - 微服务

跨平台:
  Windows / Linux / macOS 都能跑
  部署到裸机、容器、K8s、云函数都行

1.2 和老 ASP.NET 的区别

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
                      ASP.NET (Framework)      ASP.NET Core
────────────────────────────────────────────────────────────────
平台                  仅 Windows               跨平台
运行时                 .NET Framework           .NET Core / 5+
Web 服务器             IIS                      Kestrel(内置)+ 反向代理
配置                  web.config               多源配置(json/env/命令行)
依赖注入               需第三方                  内置(一等公民)
性能                  一般                     极高(TechEmpower 前列)
模块化                整体框架                  按需引入 NuGet 包
开源                  否                       是(MIT)

1.3 设计哲学

ASP.NET Core 的所有设计围绕两个核心:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
1. 中间件管道(Middleware Pipeline)
   请求像流水线一样依次经过一个个中间件
   每个中间件处理一件事(认证、日志、路由、CORS...)
   组合优于继承,按需组装

2. 依赖注入(Dependency Injection)无处不在
   所有组件通过 DI 容器管理
   面向接口编程,解耦
   内置 DI 容器,无需第三方

  理解这两点 = 理解 ASP.NET Core 的一切

二、Program.cs 与启动流程

2.1 最简启动

.NET 6+ 之后,Program.cs 用 top-level statements,几行就能启动一个 API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// 注册服务(DI)
builder.Services.AddControllers();

var app = builder.Build();

// 配置中间件管道
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();   // 启动,开始监听

这五行做了非常多的事。拆开看。

2.2 启动流程详解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
1. WebApplication.CreateBuilder(args)
   创建 WebApplicationBuilder,预配置:
   - Kestrel(Web 服务器)
   - 配置系统(appsettings.json、环境变量、命令行)
   - 日志系统
   - 依赖注入容器
   - 泛型 Host
   → 返回 builder,你通过它继续配置

2. builder.Services.AddXxx()
   向 DI 容器注册服务
   这里注册的东西,后面能通过构造函数注入
   例:AddControllers、AddDbContext、AddSwagger、AddHttpClient

3. builder.Build()
   构建 WebApplication 实例
   此后不能再注册服务(容器已固化)
   转入"配置中间件"阶段

4. app.UseXxx() / app.MapXxx()
   组装中间件管道、映射端点
   顺序很重要!(后面详述)

5. app.Run()
   启动应用,开始监听 HTTP 请求
   阻塞主线程,直到应用停止
1
2
3
4
5
关键分水岭:builder.Build()
  Build 之前 → 注册阶段(配服务、配配置)
  Build 之后 → 运行阶段(配中间件、配管道)

  不能在 Build 后注册服务,不能在 Build 前加中间件

2.3 两个阶段的清晰对照

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var builder = WebApplication.CreateBuilder(args);

// ========== 注册阶段(builder)==========
builder.Services.AddControllers();
builder.Services.AddDbContext<AppDb>(opt => opt.UseSqlServer("..."));
builder.Services.AddScoped<IUserService, UserService>();
builder.Logging.AddConsole();

var app = builder.Build();   // ← 分水岭

// ========== 管道阶段(app)==========
app.UseExceptionHandler();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();   // ← 启动

三、中间件管道(核心)

中间件管道是 ASP.NET Core 处理请求的主干道。每个请求依次穿过所有中间件,响应再原路返回。

3.1 管道模型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
请求 →  [中间件1] → [中间件2] → [中间件3] → [端点处理] → 返回
         │           │           │               │
         │           │           │               │
响应 ←  [中间件1] ← [中间件2] ← [中间件3] ←─────┘

  每个中间件:
    - 处理请求(进)→ 调用下一个 → 处理响应(出)
    - 可以决定是否调用下一个(短路)

  像洋葱模型:层层进入,层层出来

3.2 三种写法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 写法一:app.Use —— 最通用,可以调用下一个
app.Use(async (context, next) =>
{
    // 进:处理请求(before)
    Console.WriteLine("请求进来");
    await next();           // 调用下一个中间件
    // 出:处理响应(after)
    Console.WriteLine("响应出去");
});

// 写法二:app.Run —— 终结中间件,不调用下一个(管道终点)
app.Run(async context =>
{
    await context.Response.WriteAsync("Hello");
});

// 写法三:app.Map —— 按路径分支
app.Map("/api", apiApp =>
{
    apiApp.Run(async ctx => await ctx.Response.WriteAsync("API"));
});

3.3 顺序非常重要

中间件的注册顺序决定执行顺序,错了会导致功能异常:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// ✅ 正确顺序(官方推荐)
app.UseExceptionHandler();   // 1. 异常处理(最外层,兜底)
app.UseHsts();               // 2. HSTS
app.UseHttpsRedirection();   // 3. HTTP→HTTPS
app.UseStaticFiles();        // 4. 静态文件
app.UseRouting();            // 5. 路由匹配
app.UseCors();               // 6. CORS
app.UseAuthentication();     // 7. 认证(谁是谁)
app.UseAuthorization();      // 8. 授权(能干什么)
app.UseSession();            // 9. Session(按需)
app.MapControllers();        // 10. 端点执行
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
为什么顺序重要:
  UseRouting 必须在 UseAuthorization 之前
    → 先匹配出端点,才能对端点做授权检查

  UseAuthentication 必须在 UseAuthorization 之前
    → 先认证身份(填充 User),才能判断权限

  UseExceptionHandler 必须在最前面
    → 任何中间件抛异常都能被它兜住

  记忆:异常 → 协议 → 静态 → 路由 → CORS → 认证 → 授权 → 端点

3.4 自定义中间件

复杂中间件建议封装成类,而不是内联 lambda:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 自定义中间件类(约定:有 InvokeAsync 方法)
public class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimingMiddleware> _logger;

    public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var sw = Stopwatch.StartNew();
        try
        {
            await _next(context);   // 调下一个
        }
        finally
        {
            sw.Stop();
            _logger.LogInformation("{Method} {Path} 耗时 {Ms}ms",
                context.Request.Method, context.Request.Path, sw.ElapsedMilliseconds);
        }
    }
}

// 扩展方法(优雅注册)
public static class RequestTimingExtensions
{
    public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder app)
    {
        return app.UseMiddleware<RequestTimingMiddleware>();
    }
}

// Program.cs 里用
app.UseRequestTiming();

四、依赖注入(核心)

ASP.NET Core 的 DI 是一等公民——所有组件都通过 DI 管理。理解 DI 是写出松耦合、可测试代码的关键。

4.1 三种生命周期

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 1. Transient —— 每次请求都新建(最轻量,最常用)
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
// 每次注入都拿新实例

// 2. Scoped —— 每个 HTTP 请求一个(请求内共享)
builder.Services.AddScoped<IUserService, UserService>();
// 同一个 HTTP 请求内拿到同一个实例,请求结束释放
// EF Core 的 DbContext 默认是 Scoped

// 3. Singleton —— 全局唯一(应用生命周期)
builder.Services.AddSingleton<IClock, SystemClock>();
// 所有请求共享一个实例,应用停止才释放
1
2
3
4
5
6
7
8
生命周期选择:
  Transient  → 轻量、无状态的服务
  Scoped     → 需要在一次请求内共享(DbContext、当前用户上下文)
  Singleton  → 全局共享、线程安全、重资源(缓存、配置、连接池)

  ⚠️ 陷阱:
    Singleton 不能依赖 Scoped 服务(Scoped 在请求间释放,Singleton 持有会出错)
    反过来可以:Scoped 可以依赖 Singleton/Transient

4.2 注册方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 接口到实现
builder.Services.AddScoped<IUserService, UserService>();

// 多实现(注入时用 IEnumerable<IUserService> 拿全部)
builder.Services.AddScoped<INotifier, EmailNotifier>();
builder.Services.AddScoped<INotifier, SmsNotifier>();

// 工厂注册(需要复杂构造)
builder.Services.AddScoped<IConnectionFactory>(sp =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    return new ConnectionFactory(config.GetConnectionString("Db"));
});

// 自身注册(不需要接口)
builder.Services.AddScoped<UserService>();

// TryAdd(不存在才注册,避免覆盖)
builder.Services.TryAddScoped<IUserService, UserService>();

4.3 注入方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 构造函数注入(推荐,最常用)
public class UserController : ControllerBase
{
    private readonly IUserService _userService;
    private readonly ILogger<UserController> _logger;

    public UserController(IUserService userService, ILogger<UserController> logger)
    {
        _userService = userService;
        _logger = logger;
    }
}

// [FromServices] 注入(Minimal API、单个方法用)
app.MapGet("/users", ([FromServices] IUserService svc) => svc.GetAll());

// IServiceProvider 手动解析(少用,需要时)
public class Worker
{
    private readonly IServiceProvider _sp;
    public Worker(IServiceProvider sp) => _sp = sp;

    public void DoWork()
    {
        using var scope = _sp.CreateScope();   // 手动创建 Scope
        var svc = scope.ServiceProvider.GetRequiredService<IUserService>();
        svc.DoSomething();
    }
}

4.4 一个完整示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 接口
public interface IUserService
{
    Task<User> GetUserAsync(int id);
}

// 实现
public class UserService : IUserService
{
    private readonly AppDbContext _db;          // Scoped
    private readonly ICache _cache;             // Singleton
    private readonly ILogger<UserService> _logger;

    // 构造函数注入三个依赖
    public UserService(AppDbContext db, ICache cache, ILogger<UserService> logger)
    {
        _db = db; _cache = cache; _logger = logger;
    }

    public async Task<User> GetUserAsync(int id) => await _db.Users.FindAsync(id);
}

// 注册
builder.Services.AddDbContext<AppDbContext>();      // Scoped
builder.Services.AddSingleton<ICache, MemoryCache>(); // Singleton
builder.Services.AddScoped<IUserService, UserService>(); // Scoped

// 使用
public class UserController : ControllerBase
{
    private readonly IUserService _userService;
    public UserController(IUserService userService) => _userService = userService;

    [HttpGet("{id}")]
    public async Task<User> Get(int id) => await _userService.GetUserAsync(id);
}

五、配置系统

ASP.NET Core 的配置非常强大——多源、分层、强类型。

5.1 配置源

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 默认按顺序读取(后者覆盖前者):
// 1. appsettings.json
// 2. appsettings.{Environment}.json   (如 appsettings.Production.json)
// 3. 用户机密(开发环境,secrets.json)
// 4. 环境变量
// 5. 命令行参数

// 读取配置
var connStr = builder.Configuration.GetConnectionString("DefaultConnection");
var name = builder.Configuration["AppName"];
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// appsettings.json
{
  "AppName": "MyBlog",
  "ConnectionStrings": {
    "DefaultConnection": "Server=...;Database=...;"
  },
  "Jwt": {
    "Issuer": "jiwei.space",
    "ExpiresMinutes": 60,
    "Secret": "..."
  }
}

5.2 强类型选项(Options 模式)

不要到处用字符串 key 读配置,用强类型 Options:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1. 定义配置类
public class JwtOptions
{
    public string Issuer { get; set; }
    public int ExpiresMinutes { get; set; }
    public string Secret { get; set; }
}

// 2. 绑定
builder.Services.Configure<JwtOptions>(builder.Configuration.GetSection("Jwt"));

// 3. 注入使用
public class TokenService
{
    private readonly JwtOptions _jwt;
    public TokenService(IOptions<JwtOptions> options) => _jwt = options.Value;

    public string CreateToken()
    {
        // _jwt.Issuer / _jwt.ExpiresMinutes / _jwt.Secret
    }
}
1
2
3
4
5
IOptions<T>          — 单例,启动时绑定,值固定
IOptionsSnapshot<T>  — 每个请求重新绑定(配置热更新生效)
IOptionsMonitor<T>   — 单例,但能监听变化

  需要配置热更新 → 用 IOptionsSnapshot

六、日志

6.1 基本用法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class UserService
{
    private readonly ILogger<UserService> _logger;

    public UserService(ILogger<UserService> logger) => _logger = logger;

    public void Login(string username)
    {
        _logger.LogInformation("用户 {Username} 登录", username);
        _logger.LogWarning("用户 {Username} 登录失败次数过多", username);
        _logger.LogError(ex, "登录异常 {Username}", username);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
日志级别(从低到高):
  Trace < Debug < Information < Warning < Error < Critical

  生产通常设 Information 或 Warning(过滤掉 Debug/Trace)

结构化日志(重要):
  ✅ _logger.LogInformation("用户 {Username} 登录", username);
     → 占位符 {Username} 是结构化字段,日志系统能按字段检索
  
  ❌ _logger.LogInformation($"用户 {username} 登录");
     → 字符串插值,丢失结构化信息,无法按字段查询

6.2 配置

1
2
3
4
5
6
7
8
9
// appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}
1
2
3
4
// Program.cs 加日志提供程序
builder.Logging.AddConsole();
builder.Logging.AddDebug();
builder.Logging.AddSeq();   // Serlog Seq 等

七、Host 与 Kestrel

7.1 泛型 Host

ASP.NET Core 跑在泛型 Host 之上,统一管理:

1
2
3
4
5
6
7
8
9
Host 管理的东西:
  - 依赖注入容器
  - 配置
  - 日志
  - 生命周期(启动、停止)
  - 后台服务(IHostedService)

  WebApplication.CreateBuilder 自动配好了一个带 Web 功能的 Host
  Worker Service 用纯 Host(无 Web)

7.2 Kestrel(内置 Web 服务器)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Kestrel 是 ASP.NET Core 自带的跨平台 Web 服务器:
  ✓ 跨平台(Windows/Linux/macOS)
  ✓ 极高性能(基于 Pipelines、异步全链路)
  ✓ 内置,不需要 IIS/Nginx 也能跑

生产部署架构:
  客户端 → Nginx(反向代理,HTTPS)→ Kestrel(HTTP)

  为什么前面要 Nginx:
    - HTTPS 终端(Kestrel 处理 HTTP)
    - 静态文件、缓存、限流
    - 更成熟的安全防护
    (详见本站 Nginx 系列)
1
2
3
4
5
6
// 配置 Kestrel
builder.WebHost.ConfigureKestrel(options =>
{
    options.Limits.MaxConcurrentConnections = 10000;
    options.Limits.MaxRequestBodySize = 10 * 1024 * 1024;  // 10MB
});

八、环境与配置

8.1 环境标识

1
2
3
4
5
6
7
8
通过环境变量 ASPNETCORE_ENVIRONMENT 设置:
  Development   — 开发(本地)
  Staging       — 预发布
  Production    — 生产

  不同环境加载不同配置文件:
    Development → appsettings.json + appsettings.Development.json
    Production  → appsettings.json + appsettings.Production.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 代码里判断环境
var env = builder.Environment;

if (env.IsDevelopment())
{
    app.UseSwagger();
    app.UseDeveloperExceptionPage();   // 开发显示详细错误
}
else
{
    app.UseExceptionHandler("/error"); // 生产用友好错误页
    app.UseHsts();
}

8.2 启动配置

1
2
3
4
5
6
7
8
# 设置环境
export ASPNETCORE_ENVIRONMENT=Production

# 设置监听端口
export ASPNETCORE_URLS=http://0.0.0.0:5000

# 启动
dotnet run

九、小结

本文是 ASP.NET Core 系列的开篇,建立了整体认知:

  • 定位:跨平台、高性能、模块化的现代 Web 框架
  • 两大支柱:中间件管道(处理请求)+ 依赖注入(管理组件)
  • 启动流程CreateBuilder → 注册服务 → Build → 配置中间件 → Run
  • 中间件管道:请求像洋葱层层穿过,顺序非常重要
  • 依赖注入:三种生命周期(Transient/Scoped/Singleton),构造函数注入
  • 配置:多源 + 强类型 Options 模式
  • 日志:ILogger 结构化日志
  • Host/Kestrel:泛型 Host 管理生命周期,Kestrel 是高性能内置服务器

核心心法:理解了中间件管道和依赖注入,ASP.NET Core 就理解了一半。 后续所有功能(路由、认证、EF Core)都是建立在这两个基础上的。

下一篇将讲路由与控制器:URL 如何匹配到代码、数据如何绑定、请求如何验证。