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

📅 2026/8/1 4:51:16 👁️ 阅读次数 📝 编程学习
.NET 8 Web开发入门(三):解构引擎——依赖注入(DI)与中间件管道

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

在前两篇中,我们搭建了第一个ASP.NET Core应用,并了解了项目结构。今天,我们要深入引擎盖下,看看驱动整个框架运转的两个核心机制:依赖注入(Dependency Injection, DI)中间件管道(Middleware Pipeline)。理解它们,就像拿到了魔法世界的钥匙,能让你从“会写”飞跃到“懂写”。### 为什么需要依赖注入(DI)?想象一下,你正在开发一个订单服务,它需要发送邮件通知。最直接的做法是:csharppublic class OrderService{ private EmailSender _emailSender = new EmailSender(); public void Process(Order order) { // 处理订单... _emailSender.Send(order.CustomerEmail, "订单已确认"); }}这看起来没问题,但隐藏着几个隐患:1.耦合度高OrderService直接依赖具体的EmailSender类。如果明天要换成短信通知,就要修改OrderService的代码。2.难以测试:单元测试时,你无法轻易地用假邮件发送器替换真实的。3.生命周期管理困难EmailSender是否应该为单例?连接池如何管理?依赖注入的核心思想是:不要自己创建依赖,而是声明你“需要什么”,由容器在运行时“给予”你。这遵循了“控制反转”(IoC)原则。### .NET 8 中的依赖注入基础ASP.NET Core 内置了一个轻量级、功能强大的 DI 容器。我们在Program.cs中使用builder.Services来注册服务。服务生命周期是 DI 中最关键的概念,决定了服务的实例何时创建和销毁:-Transient(瞬时):每次请求时都创建一个新实例。适合轻量、无状态的服务。-Scoped(作用域):在同一个 HTTP 请求内,只创建一个实例。适合数据库上下文(如 EF Core 的DbContext)。-Singleton(单例):整个应用只创建一个实例。适合配置、日志等全局对象。下面是一个完整的示例,展示了如何注册和使用不同生命周期的服务。csharp// 1. 定义服务接口和实现public interface IGreetingService{ string Greet(string name);}public class FormalGreetingService : IGreetingService{ private readonly Guid _instanceId = Guid.NewGuid(); public string Greet(string name) => $"[{_instanceId}] 您好,{name},欢迎光临!";}public class CasualGreetingService : IGreetingService{ public string Greet(string name) => $"嘿,{name},最近咋样?";}// 2. 在 Program.cs 中注册服务var builder = WebApplication.CreateBuilder(args);// 注册接口到具体实现,并指定生命周期builder.Services.AddTransient<IgreetingService, FormalGreetingService>(); // 每次请求都新建builder.Services.AddScoped<IGreetingService, CasualGreetingService>(); // 同一请求内共享builder.Services.AddSingleton<IGreetingService, FormalGreetingService>(); // 全局唯一var app = builder.Build();// 3. 通过构造函数注入(推荐方式)app.MapGet("/greet", (IGreetingService greetingService) =>{ return greetingService.Greet("张三");});app.Run();代码解析:- 我们定义了IGreetingService接口和两个实现。- 在builder.Services中,我们依次注册了三种生命周期。注意:后注册的会覆盖先注册的,所以最终IGreetingService指向的是FormalGreetingService(Singleton)。- 在MapGet中,框架自动将IGreetingService注入到 lambda 表达式的参数中。这就是构造函数/参数注入。>最佳实践:不要为了用 DI 而用 DI。对于简单的、不会变化的依赖,直接new也未尝不可。DI 的优势在大型应用中尤为明显。### 中间件管道:处理请求的“流水线”如果说 DI 是应用的血肉,那么中间件管道就是应用的骨架。中间件(Middleware)是处理 HTTP 请求和响应的一系列组件,它们像洋葱一样一层层包裹着核心逻辑。管道工作流程:1. 请求从外部进入,依次经过注册的中间件。2. 每个中间件可以处理请求,然后调用下一个中间件(通过next委托),或者短路(直接返回响应,不调用下一个)。3. 响应从最内层逐层向外传递。看下面这个经典的“日志-异常-路由”管道示例:csharpvar app = builder.Build();// 1. 第一个中间件:记录请求开始app.Use(async (context, next) =>{ Console.WriteLine($"请求开始: {context.Request.Path}"); await next(); // 调用下一个中间件 Console.WriteLine($"请求结束: {context.Response.StatusCode}");});// 2. 第二个中间件:异常处理(短路示例)app.Use(async (context, next) =>{ try { await next(); } catch (Exception ex) { Console.WriteLine($"捕获异常: {ex.Message}"); context.Response.StatusCode = 500; await context.Response.WriteAsync("服务器内部错误"); }});// 3. 终结点中间件(短路示例)app.Run(async context =>{ // 如果请求路径是 /secret,直接返回,不再往下走 if (context.Request.Path == "/secret") { context.Response.StatusCode = 401; await context.Response.WriteAsync("未经授权"); return; // 注意:这里没有调用 next() } await context.Response.WriteAsync("Hello, Middleware Pipeline!");});app.Run();代码解析:-app.Use()用于添加一个中间件,它接收contextnext两个参数。next是调用管道中下一个中间件的委托。-app.Run()添加一个终结中间件,它不接收next参数,表示它是管道的终点。- 示例中,第二个中间件用try-catch包裹了next(),这意味着它捕获了后续所有中间件抛出的异常——这是全局异常处理的雏形。- 在终结点中间件中,我们演示了“短路”:当访问/secret时,直接返回 401,不执行后续代码。内置中间件:ASP.NET Core 提供了许多现成的中间件,如:-UseRouting()/UseEndpoints():路由和终结点执行。-UseAuthentication()/UseAuthorization():认证和授权。-UseStaticFiles():服务静态文件。-UseCors():跨域支持。在Program.cs中,它们有特定的顺序要求(比如认证必须在授权之前)。这就是为什么你会看到模板代码中有一长串app.Use...()调用。### 组合拳:DI + 中间件 = 强大的自定义功能DI 和中间件经常配合使用。中间件可以通过构造函数注入服务,但要小心生命周期问题(比如不要注入 Scoped 服务到 Singleton 中间件)。下面是一个更实用的示例:创建一个请求日志中间件,它使用 DI 注入一个日志服务,记录每次请求的耗时。csharp// 1. 自定义中间件类public class RequestTimingMiddleware{ private readonly RequestDelegate _next; private readonly ILogger<RequestTimingMiddleware> _logger; // 构造函数注入:RequestDelegate 和 ILogger 是框架自动提供的 public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { var stopwatch = System.Diagnostics.Stopwatch.StartNew(); // 调用管道中的下一个中间件 await _next(context); stopwatch.Stop(); _logger.LogInformation($"请求 {context.Request.Path} 耗时 {stopwatch.ElapsedMilliseconds} ms"); }}// 2. 扩展方法,方便在 Program.cs 中调用public static class RequestTimingMiddlewareExtensions{ public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder builder) { return builder.UseMiddleware<RequestTimingMiddleware>(); }}// 3. 在 Program.cs 中使用var builder = WebApplication.CreateBuilder(args);builder.Services.AddLogging(); // 确保日志服务已注册var app = builder.Build();// 使用自定义中间件app.UseRequestTiming();app.MapGet("/", () => "Hello World!");app.Run();代码解析:-RequestTimingMiddleware类有一个构造函数,接收RequestDelegate(代表下一个中间件)和ILogger<T>(日志服务)。框架通过 DI 自动解析这些依赖。-InvokeAsync方法是中间件的核心,它必须存在,且返回Task。- 我们创建了一个扩展方法UseRequestTiming,这使代码更简洁,也符合 ASP.NET Core 的惯例。### 总结今天我们揭开了 .NET 8 Web 开发的两个核心引擎:1.依赖注入(DI)解决了组件之间的耦合问题,通过接口和容器,让代码更灵活、可测试。掌握三种生命周期(Transient、Scoped、Singleton)是正确使用 DI 的关键。2.中间件管道提供了一种可组合、可复用的方式来处理 HTTP 请求。通过Use()Run()方法,你可以控制请求的流向,实现日志、认证、异常处理等横切关注点。进阶思考:- 尝试在中间件中注入IServiceScopeFactory来手动创建作用域,以解决生命周期冲突。- 研究一下UseWhenMapWhen,它们可以按条件分支管道。- 使用dotnet new web创建一个空模板,然后从零开始添加中间件,观察管道的变化。掌握这两大引擎,你已经从“使用框架”走向了“理解框架”。下一期,我们将深入路由和模型绑定,看看请求参数是如何被“魔法”地绑定到方法参数上的。继续加油!