.NET 8 Web开发入门(三):解构引擎——依赖注入(DI)与中间件管道

大家好,我是码农刚子。感谢各位朋友对我前两篇入门教程文章的热烈反馈和宝贵支持!🙏 看到评论区里说"通俗易懂、很容易理解","很详细","写的很好,继续努力"。以及给我的一些建议非常专业,我会认真消化,尽量在后面独立成章来回应你的期待。同时也要感谢所有默默点赞(支持(1)背后的你们)和持续关注的读者。
好了,闲言少叙,进入正题------第三篇:依赖注入与中间件。这篇会带大家彻底搞懂DI容器的作用域、服务生命周期,以及请求管道的核心中间件原理。希望继续保持前两篇的"通俗详细"风格,不辜负大家的每一份支持。上干货!👇

一、前言:从"作坊"到"工厂"

在上一篇文章中,我们学会了C#的现代语法,就像掌握了制造精密零件的技术。现在,我们需要把这些零件组装成一台能运转的发动机。

在ASP.NET Core中,有两样东西构成了这台发动机的骨架:依赖注入(DI)中间件

如果你不理解它们,你写的代码可能会变成紧紧缠绕的一团乱麻(我们称之为"面条代码"),难以测试、难以修改。理解了它们,你就掌握了现代Web开发的"设计模式之钥"。

二、灵魂机制:依赖注入(DI)

2.1 为什么要"注入"?------解决紧耦合

假设你需要在一个API中记录日志。最直观的写法可能是直接在代码里 new 一个对象:

csharp 复制代码
app.MapGet("/bad", () =>
{
    var logger = new FileLogger(); // 直接依赖具体的实现类
    logger.Log("这是一条日志");
    return "日志已记录";
});

这种写法看似简单,实则隐患重重:

  1. 紧耦合 :你的API代码死死地绑定了 FileLogger。如果哪天老板说"改成存数据库",你得修改每一处 new FileLogger()
  2. 难以测试:做单元测试时,你不想真的去写文件,想用一个假的记录器,但现在你无法替换。

依赖注入的核心思想是:"不要自己new,需要什么向容器要"(控制反转,IoC)。

2.2 服务的三生三世:生命周期

在.NET的DI容器中,注册的服务有三种主要生命周期。这是新手最容易踩坑的地方,请务必理解:

  1. Transient(瞬态)用完即弃。每次请求该服务,容器都会给你一个全新的实例。适合轻量级、无状态的服务(如简单的计算器、格式化工具)。
  2. Scoped(范围)一次请求一生 。在一次HTTP请求范围内,无论你在多少个地方请求它,拿到的都是同一个实例。这是Web开发中最常用的模式,特别是用于数据库上下文(DbContext)
  3. Singleton(单例)万世一系 。整个应用程序生命周期内,只存在一个实例。适合全局缓存、全局配置。注意:单例服务必须是线程安全的!

2.3 实战:构建一个性能监控服务

我们来写一个真实的案例:统计API的执行耗时。

第一步:定义契约(接口) 良好的架构总是面向接口编程。

csharp 复制代码
// IPerformanceTracker.cs
public interface IPerformanceTracker
{
    void Start();
    void Stop();
    long GetElapsedTime();
}

第二步:实现服务

csharp 复制代码
// PerformanceTracker.cs
public class PerformanceTracker : IPerformanceTracker
{
    private Stopwatch _stopwatch = new Stopwatch();

    public void Start() => _stopwatch.Restart();
    
    public void Stop() => _stopwatch.Stop();

    public long GetElapsedTime() => _stopwatch.ElapsedMilliseconds;
}

第三步:在Program.cs中注册服务

csharp 复制代码
var builder = WebApplication.CreateBuilder(args);

// --- 注册服务 ---
// 这里我们使用 Scoped,因为耗时统计通常是针对单个请求的
builder.Services.AddScoped<IPerformanceTracker, PerformanceTracker>();

// 添加Swagger等基础服务
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// ... 中间件配置 ...

第四步:在API中注入并使用 在Minimal API中,我们通过方法参数注入服务。

csharp 复制代码
app.MapGet("/test-performance", (IPerformanceTracker tracker) =>
{
    tracker.Start();
    
    // 模拟耗时操作
    Thread.Sleep(500); 
    
    tracker.Stop();
    
    return $"接口执行耗时: {tracker.GetElapsedTime()} ms";
});

架构师视角的深意 : 注意看,我们的API代码里完全没有 new PerformanceTracker()。这意味着,如果明天我们需要升级监控逻辑(比如加上日志记录),我们只需要修改 PerformanceTracker.cs 类,而API接口的代码一行都不用动。这就是解耦带来的维护性提升。

三、传动装置:中间件管道

如果说DI是提供动力的气缸,那么中间件就是负责传递动力的齿轮和传送带。

3.1 管道模型:俄罗斯套娃

ASP.NET Core 处理HTTP请求的方式,就像水流通过一系列过滤层。

  1. 请求进入管道。
  2. 经过一个个中间件。
  3. 中间件可以在处理做事(如记录请求日志)。
  4. 中间件调用 next() 将请求传给下一个中间件。
  5. 到达最终处理逻辑(你的API代码)。
  6. 响应沿着管道反向流出。
  7. 中间件可以在处理做事(如记录响应日志、处理异常)。

3.2 编写你的第一个自定义中间件

我们来写一个最简单的中间件:请求计时器。它将在控制台打印每个请求的耗时。

csharp 复制代码
var app = builder.Build();

// --- 自定义中间件 ---
app.Use(async (context, next) =>
{
    var stopwatch = new Stopwatch();
    stopwatch.Start();
    
    Console.WriteLine($"[中间件] 请求开始: {context.Request.Path}");

    // 关键步骤:调用下一个中间件
    // 这里使用 await 等待后续管道全部执行完毕
    await next(context); 

    stopwatch.Stop();
    
    Console.WriteLine($"[中间件] 请求结束: {context.Request.Path}, 耗时: {stopwatch.ElapsedMilliseconds}ms");
});

// 确保有Swagger中间件
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.MapGet("/", () => "Hello World!");

app.Run();

运行这段代码,并在浏览器访问 http://localhost:5000/,你会看到控制台输出了耗时信息。

3.3 "短路"机制:权限守门员

中间件有一个极其重要的能力:短路 。如果中间件决定不调用 next(),管道就会直接折返,后续的逻辑(如你的API代码)将不会执行。

这非常适合做权限验证。

csharp 复制代码
app.Use(async (context, next) =>
{
    // 模拟:检查Header里是否有密码
    if (!context.Request.Headers.ContainsKey("X-Secret-Key"))
    {
        // 没有密钥,直接返回401,不调用 next()
        context.Response.StatusCode = 401;
        await context.Response.WriteAsync("抱歉,你无权访问!");
        return; // 结束处理,管道短路
    }

    // 有密钥,放行
    await next(context);
});

这个特性让我们可以把横切关注点(如日志、权限、异常处理)从业务代码中剥离出来,放在管道的最外层统一处理。

四、DI与中间件的完美结合

作为本篇的压轴,我们将展示如何在一个中间件中使用依赖注入的服务。这是架构设计中非常常见的模式:在中间件里实现全局的异常捕获或性能监控

让我们把刚才的 IPerformanceTracker 服务集成到中间件里。

csharp 复制代码
// 注册服务
builder.Services.AddScoped<IPerformanceTracker, PerformanceTracker>();

var app = builder.Build();

// 注册一个使用了DI的中间件
// 注意:这里我们不能直接在 Use 方法里通过参数注入 Scoped 服务,
// 因为中间件构造函数是在应用启动时执行的(Singleton行为),
// 但我们需要在请求上下文中获取 Scoped 服务。
// 以下是正确的写法:

app.Use(async (context, next) =>
{
    // 1. 从 HttpContext.RequestServices 中获取当前请求的服务容器
    var tracker = context.RequestServices.GetRequiredService<IPerformanceTracker>();
    
    tracker.Start();
    
    await next(context); // 执行后续管道
    
    tracker.Stop();
    
    // 假设我们想把耗时加到响应头里
    context.Response.Headers.Append("X-Response-Time", $"{tracker.GetElapsedTime()}ms");
});

app.MapGet("/heavy-task", async (IPerformanceTracker tracker) => 
{
    // 在API内部也可以再次注入使用,且因为是 Scoped,拿到的是同一个实例
    await Task.Delay(1000);
    return "任务完成";
});

app.Run();

关键知识点context.RequestServices 是访问当前请求作用域内服务的入口。虽然Minimal API支持直接在参数里注入,但在中间件这种早期阶段,我们必须手动从 HttpContext 中拉取服务。

五、常见误区与架构师建议

在多年的架构生涯中,我见过很多新手在使用DI和中间件时犯过以下错误,这里逐一提醒:

5.1 服务生命周期陷阱:Capturing Dependencies(依赖捕获)

错误做法 :在一个 Singleton 服务中注入了一个 Scoped 服务。 后果 :Scoped 服务本该在一次请求后销毁,但因为被 Singleton 服务长期持有,它变成了事实上的 Singleton。这会导致你的 DbContext 无法正确释放,内存泄漏,甚至并发错误。 原则 :服务依赖的方向应该是:Transient -> Scoped -> Singleton。或者 Scoped -> Scoped。永远不要让生命周期长的服务依赖生命周期短的服务。

5.2 中间件顺序很重要

中间件的注册顺序直接决定了执行顺序。

  • UseExceptionHandler / UseDeveloperExceptionPage 应该放在最前面,这样才能捕获后续所有中间件的异常。
  • UseStaticFiles 应该放在 UseAuthorization 之前,否则静态文件(如图片、CSS)也需要权限验证,这通常是不必要的性能损耗。
  • UseSwagger 通常放在开发环境判断内部。

六、总结与下篇预告

恭喜你!读到这里,你已经触摸到了ASP.NET Core的骨架。

  • 依赖注入(DI):解耦的神器,让代码结构清晰,易于测试。记住 Transient、Scoped、Singleton 三种生命周期的区别。
  • 中间件 :请求的筛子和过滤器,用于处理横切逻辑。记住 next() 是通往下一关的钥匙。

现在,我们的"引擎"已经组装完毕,具备了处理请求的核心能力。但是,引擎需要"燃料"才能源源不断地输出动力。在Web开发中,最核心的燃料就是数据

下一篇预告

在第四篇文章中,我们将连接数据库,引入 Entity Framework Core (EF Core)。你将学会如何用 C# 代码定义数据库结构(Code First),如何进行数据迁移,以及如何通过 EF Core 进行高效的增删改查。数据持久化的大门即将打开,敬请期待!

相关推荐
zhuzicc3 天前
Dubbo @Autowired 注入同模块接口,到底走的是本地调用还是 RPC?源码给你答案(Dubbo @Service注解的双重注册机制)
rpc·autowired·dubbo·依赖注入·java面试·spring ioc·dubbo源码分析
阿昌喜欢吃黄桃3 天前
RocketMq事务消息原理
java·中间件·消息队列·rocketmq·mq
半夜修仙4 天前
延迟队列的介绍及常见问题
java·数据库·中间件·rabbitmq
手握风云-4 天前
一条消息的旅程:RabbitMQ 学习与实践(一)
中间件·rabbitmq
RH2312115 天前
2026.6.8Linux
java·数据库·中间件
理人综艺好会6 天前
双Token机制在实际项目中的应用与实践
中间件·token
番茄去哪了6 天前
神领物流面试题(一)
java·大数据·中间件
念何架构之路6 天前
消息中间件
中间件
都说名字长不会被发现6 天前
Spring Boot Starter 中间件账号密码加密方案设计与实现
java·spring boot·后端·中间件
瀚高PG实验室7 天前
java中间件无法连接数据库
java·数据库·中间件·瀚高数据库