专栏导航
← 上一篇:互联网与 Web 应用简介
← 第一篇:编程世界初探
依赖注入与中间件 - ASP.NET Core 核心概念
- 专栏导航
-
- [一、依赖注入(Dependency Injection)](#一、依赖注入(Dependency Injection))
- 二、中间件(Middleware)
-
- [2.1 什么是中间件?](#2.1 什么是中间件?)
- [2.2 内置中间件](#2.2 内置中间件)
- [2.3 中间件顺序很重要](#2.3 中间件顺序很重要)
- [2.4 自定义中间件](#2.4 自定义中间件)
-
- [方式 1:使用 Lambda 表达式](#方式 1:使用 Lambda 表达式)
- [方式 2:使用中间件类](#方式 2:使用中间件类)
- [2.5 终端中间件](#2.5 终端中间件)
- 三、综合示例:图书管理系统升级
-
- [3.1 项目结构](#3.1 项目结构)
- [3.2 完整的 Program.cs](#3.2 完整的 Program.cs)
- 四、单元测试与依赖注入
-
- [4.1 为什么需要单元测试?](#4.1 为什么需要单元测试?)
- [4.2 创建测试项目](#4.2 创建测试项目)
- [4.3 Mock 服务](#4.3 Mock 服务)
- 五、本周小结
上一篇文章我们创建了第一个 ASP.NET Core Web API,但代码中还使用了一个静态的
List<Book>来存储数据。这显然不是生产环境的最佳实践。今天,我们将学习 ASP.NET Core 的两大核心概念:依赖注入(DI)和中间件(Middleware),它们是构建高质量、可维护应用的关键技术。
一、依赖注入(Dependency Injection)
1.1 什么是依赖注入?
依赖注入(DI)是一种设计模式,用于实现控制反转(IoC)。简单来说,就是"不要自己创建依赖的对象,而是让外部注入给你"。
问题:没有依赖注入
csharp
// ❌ 不好的做法:直接在控制器中创建服务
public class BooksController : ControllerBase
{
private BookService _bookService;
public BooksController()
{
_bookService = new BookService(); // 硬编码依赖
}
}
问题:
- 控制器与 BookService 强耦合
- 无法方便地替换实现(如换成另一个服务)
- 难以进行单元测试
解决:使用依赖注入
csharp
// ✅ 好的做法:通过构造函数注入
public class BooksController : ControllerBase
{
private readonly BookService _bookService;
public BooksController(BookService bookService)
{
_bookService = bookService; // 外部注入
}
}
优势:
- 降低耦合度
- 方便替换实现
- 易于单元测试
- 符合单一职责原则
1.2 服务生命周期
在 ASP.NET Core 中,注册的服务有三种生命周期:
| 生命周期 | 说明 | 使用场景 |
|---|---|---|
| Transient | 每次请求都创建新实例 | 轻量级、无状态服务 |
| Scoped | 每次 HTTP 请求创建一个实例 | 与请求相关的服务 |
| Singleton | 整个应用生命周期只有一个实例 | 全局共享的服务 |
生命周期示例
csharp
// Services/LifetimeService.cs
public class LifetimeService
{
private readonly Guid _instanceId;
public LifetimeService()
{
_instanceId = Guid.NewGuid();
}
public string GetInstanceId()
{
return _instanceId.ToString();
}
}
// Program.cs - 注册服务
var builder = WebApplication.CreateBuilder(args);
// Transient:每次注入都创建新实例
builder.Services.AddTransient<LifetimeService>();
// Scoped:每次 HTTP 请求创建一个实例
builder.Services.AddScoped<LifetimeService>();
// Singleton:整个应用只创建一个实例
builder.Services.AddSingleton<LifetimeService>();
测试结果:
第一次请求:
Transient: InstanceA
Scoped: InstanceA
Singleton: InstanceA
第二次请求:
Transient: InstanceB ← 新实例
Scoped: InstanceB ← 新实例
Singleton: InstanceA ← 同一实例
1.3 注册和使用服务
步骤 1:创建服务
csharp
// Services/BookService.cs
using MyFirstWebApp.Models;
public class BookService
{
private static List<Book> _books = new List<Book>
{
new Book { Id = 1, Title = "C# 入门", Author = "张三", Price = 89.5m },
new Book { Id = 2, Title = "ASP.NET Core 实战", Author = "李四", Price = 99.0m }
};
public List<Book> GetAllBooks()
{
return _books;
}
public Book? GetBookById(int id)
{
return _books.FirstOrDefault(b => b.Id == id);
}
public void AddBook(Book book)
{
book.Id = _books.Max(b => b.Id) + 1;
_books.Add(book);
}
public void UpdateBook(Book book)
{
var existing = _books.FirstOrDefault(b => b.Id == book.Id);
if (existing != null)
{
existing.Title = book.Title;
existing.Author = book.Author;
existing.Price = book.Price;
}
}
public void DeleteBook(int id)
{
var book = _books.FirstOrDefault(b => b.Id == id);
if (book != null)
{
_books.Remove(book);
}
}
}
步骤 2:注册服务
csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// 添加服务到容器
builder.Services.AddControllers();
builder.Services.AddScoped<BookService>(); // 注册服务
var app = builder.Build();
app.MapControllers();
app.Run();
步骤 3:在控制器中使用
csharp
// Controllers/BooksController.cs
using Microsoft.AspNetCore.Mvc;
using MyFirstWebApp.Models;
using MyFirstWebApp.Services;
[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
private readonly BookService _bookService;
// 通过构造函数注入 BookService
public BooksController(BookService bookService)
{
_bookService = bookService;
}
// GET: api/books
[HttpGet]
public ActionResult<IEnumerable<Book>> GetBooks()
{
var books = _bookService.GetAllBooks();
return Ok(books);
}
// GET: api/books/1
[HttpGet("{id}")]
public ActionResult<Book> GetBook(int id)
{
var book = _bookService.GetBookById(id);
if (book == null)
{
return NotFound($"未找到 ID 为 {id} 的图书");
}
return Ok(book);
}
// POST: api/books
[HttpPost]
public ActionResult<Book> AddBook([FromBody] Book book)
{
_bookService.AddBook(book);
return CreatedAtAction(nameof(GetBook), new { id = book.Id }, book);
}
// PUT: api/books/1
[HttpPut("{id}")]
public IActionResult UpdateBook(int id, [FromBody] Book book)
{
var existing = _bookService.GetBookById(id);
if (existing == null)
{
return NotFound();
}
_bookService.UpdateBook(book);
return NoContent();
}
// DELETE: api/books/1
[HttpDelete("{id}")]
public IActionResult DeleteBook(int id)
{
var existing = _bookService.GetBookById(id);
if (existing == null)
{
return NotFound();
}
_bookService.DeleteBook(id);
return NoContent();
}
}
1.4 接口与实现分离
更好的做法是使用接口,便于替换实现和单元测试。
csharp
// Services/IBookService.cs
public interface IBookService
{
List<Book> GetAllBooks();
Book? GetBookById(int id);
void AddBook(Book book);
void UpdateBook(Book book);
void DeleteBook(int id);
}
// Services/BookService.cs
public class BookService : IBookService
{
// ... 实现 IBookService 的所有方法
}
// Program.cs
builder.Services.AddScoped<IBookService, BookService>();
// Controllers/BooksController.cs
public class BooksController : ControllerBase
{
private readonly IBookService _bookService;
public BooksController(IBookService bookService)
{
_bookService = bookService;
}
// ...
}
二、中间件(Middleware)
2.1 什么是中间件?
**中间件(Middleware)**是 ASP.NET Core 请求管道中的组件,每个中间件都可以:
- 处理请求
- 决定是否传递给下一个中间件
- 修改请求或响应
请求管道
HTTP 请求
↓
┌─────────────────┐
│ Middleware 1 │ ◄─── 可以在这里做日志记录
├─────────────────┤
│ Middleware 2 │ ◄─── 可以在这里做身份认证
├─────────────────┤
│ Middleware 3 │ ◄─── 可以在这里做异常处理
├─────────────────┤
│ Controller │ ◄─── 最终处理请求
└─────────────────┘
↓
HTTP 响应
2.2 内置中间件
ASP.NET Core 提供了许多内置中间件:
| 中间件 | 说明 |
|---|---|
UseStaticFiles() |
提供静态文件(HTML、CSS、JS) |
UseRouting() |
路由匹配 |
UseAuthorization() |
授权检查 |
UseAuthentication() |
身份认证 |
UseHttpsRedirection() |
强制 HTTPS |
UseExceptionHandler() |
异常处理 |
2.3 中间件顺序很重要
中间件的注册顺序决定了它们的执行顺序:
csharp
var app = builder.Build();
// 1. 异常处理(应该在最前面)
app.UseExceptionHandler("/error");
// 2. HTTPS 重定向
app.UseHttpsRedirection();
// 3. 静态文件
app.UseStaticFiles();
// 4. 路由
app.UseRouting();
// 5. 认证
app.UseAuthentication();
// 6. 授权
app.UseAuthorization();
// 7. 映射控制器
app.MapControllers();
app.Run();
2.4 自定义中间件
方式 1:使用 Lambda 表达式
csharp
// Program.cs
app.Use(async (context, next) =>
{
// 请求处理之前
Console.WriteLine($"请求: {context.Request.Method} {context.Request.Path}");
// 调用下一个中间件
await next();
// 响应处理之后
Console.WriteLine($"响应状态码: {context.Response.StatusCode}");
});
方式 2:使用中间件类
csharp
// Middleware/RequestLoggingMiddleware.cs
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// 记录请求信息
_logger.LogInformation($"开始处理: {context.Request.Method} {context.Request.Path}");
var watch = System.Diagnostics.Stopwatch.StartNew();
// 调用下一个中间件
await _next(context);
watch.Stop();
// 记录响应信息
_logger.LogInformation($"完成处理: {context.Response.StatusCode} 耗时: {watch.ElapsedMilliseconds}ms");
}
}
扩展方法(方便使用):
csharp
// Extensions/RequestLoggingExtensions.cs
public static class RequestLoggingExtensions
{
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
}
使用中间件:
csharp
// Program.cs
app.UseRequestLogging(); // 自定义中间件
2.5 终端中间件
某些中间件可能终止请求管道,不再调用下一个中间件:
csharp
app.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/admin"))
{
// 直接返回响应,不继续
context.Response.StatusCode = 403;
await context.Response.WriteAsync("禁止访问");
return; // 终止管道
}
await next(); // 继续调用下一个中间件
});
三、综合示例:图书管理系统升级
3.1 项目结构
MyFirstWebApp/
├── Controllers/
│ └── BooksController.cs
├── Services/
│ ├── IBookService.cs
│ └── BookService.cs
├── Middleware/
│ └── RequestLoggingMiddleware.cs
├── Models/
│ └── Book.cs
├── Extensions/
│ └── RequestLoggingExtensions.cs
├── Program.cs
└── appsettings.json
3.2 完整的 Program.cs
csharp
using MyFirstWebApp.Extensions;
using MyFirstWebApp.Services;
var builder = WebApplication.CreateBuilder(args);
// 注册服务
builder.Services.AddControllers();
builder.Services.AddScoped<IBookService, BookService>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// 配置请求管道
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// 自定义中间件:请求日志
app.UseRequestLogging();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
四、单元测试与依赖注入
4.1 为什么需要单元测试?
使用依赖注入后,我们可以轻松地进行单元测试,而无需依赖真实的数据库或外部服务。
4.2 创建测试项目
bash
# 创建测试项目
dotnet new xunit -n MyFirstWebApp.Tests
# 添加项目引用
cd MyFirstWebApp.Tests
dotnet add reference ../MyFirstWebApp.csproj
4.3 Mock 服务
csharp
// Tests/BooksControllerTests.cs
using Moq;
using MyFirstWebApp.Controllers;
using MyFirstWebApp.Models;
using MyFirstWebApp.Services;
using Xunit;
public class BooksControllerTests
{
[Fact]
public void GetBooks_ReturnsAllBooks()
{
// Arrange(准备)
var mockService = new Mock<IBookService>();
mockService.Setup(s => s.GetAllBooks())
.Returns(new List<Book>
{
new Book { Id = 1, Title = "测试书", Author = "作者", Price = 10m }
});
var controller = new BooksController(mockService.Object);
// Act(执行)
var result = controller.GetBooks();
// Assert(断言)
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var books = Assert.IsType<List<Book>>(okResult.Value);
Assert.Single(books);
}
}
五、本周小结
核心知识点
-
依赖注入(DI)
- 什么是依赖注入和控制反转
- 服务生命周期:Transient、Scoped、Singleton
- 如何注册和使用服务
- 接口与实现分离
-
中间件(Middleware)
- 中间件的概念和作用
- 请求管道的工作原理
- 内置中间件的使用
- 自定义中间件的编写
-
最佳实践
- 使用接口提高可测试性
- 合理选择服务生命周期
- 注意中间件的注册顺序
实践成果
✅ 重构图书管理系统,使用依赖注入
✅ 创建自定义服务(BookService)
✅ 创建自定义中间件(RequestLoggingMiddleware)
✅ 理解了服务生命周期的区别
✅ 掌握了请求管道的工作原理
下周预告:第17章 将深入学习 MVC 模式,学习如何创建 Model(模型)、View(视图)和 Controller(控制器),构建完整的 Web 页面!