.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.AddTransientIgreetingService, FormalGreetingService(); // 每次请求都新建builder.Services.AddScopedIGreetingService, CasualGreetingService(); // 同一请求内共享builder.Services.AddSingletonIGreetingService, FormalGreetingService(); // 全局唯一var app builder.Build();// 3. 通过构造函数注入推荐方式app.MapGet(/greet, (IGreetingService greetingService) { return greetingService.Greet(张三);});app.Run();代码解析- 我们定义了IGreetingService接口和两个实现。- 在builder.Services中我们依次注册了三种生命周期。注意后注册的会覆盖先注册的所以最终IGreetingService指向的是FormalGreetingServiceSingleton。- 在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()用于添加一个中间件它接收context和next两个参数。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 ILoggerRequestTimingMiddleware _logger; // 构造函数注入RequestDelegate 和 ILogger 是框架自动提供的 public RequestTimingMiddleware(RequestDelegate next, ILoggerRequestTimingMiddleware 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.UseMiddlewareRequestTimingMiddleware(); }}// 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代表下一个中间件和ILoggerT日志服务。框架通过 DI 自动解析这些依赖。-InvokeAsync方法是中间件的核心它必须存在且返回Task。- 我们创建了一个扩展方法UseRequestTiming这使代码更简洁也符合 ASP.NET Core 的惯例。### 总结今天我们揭开了 .NET 8 Web 开发的两个核心引擎1.依赖注入DI解决了组件之间的耦合问题通过接口和容器让代码更灵活、可测试。掌握三种生命周期Transient、Scoped、Singleton是正确使用 DI 的关键。2.中间件管道提供了一种可组合、可复用的方式来处理 HTTP 请求。通过Use()和Run()方法你可以控制请求的流向实现日志、认证、异常处理等横切关注点。进阶思考- 尝试在中间件中注入IServiceScopeFactory来手动创建作用域以解决生命周期冲突。- 研究一下UseWhen和MapWhen它们可以按条件分支管道。- 使用dotnet new web创建一个空模板然后从零开始添加中间件观察管道的变化。掌握这两大引擎你已经从“使用框架”走向了“理解框架”。下一期我们将深入路由和模型绑定看看请求参数是如何被“魔法”地绑定到方法参数上的。继续加油