依赖注入与中间件 - ASP.NET Core 核心概念

专栏导航

← 上一篇:互联网与 Web 应用简介

← 第一篇:编程世界初探

专栏目录

依赖注入与中间件 - ASP.NET Core 核心概念

  • 专栏导航
    • [一、依赖注入(Dependency Injection)](#一、依赖注入(Dependency Injection))
      • [1.1 什么是依赖注入?](#1.1 什么是依赖注入?)
      • [1.2 服务生命周期](#1.2 服务生命周期)
      • [1.3 注册和使用服务](#1.3 注册和使用服务)
        • [步骤 1:创建服务](#步骤 1:创建服务)
        • [步骤 2:注册服务](#步骤 2:注册服务)
        • [步骤 3:在控制器中使用](#步骤 3:在控制器中使用)
      • [1.4 接口与实现分离](#1.4 接口与实现分离)
    • 二、中间件(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);
    }
}

五、本周小结

核心知识点

  1. 依赖注入(DI)

    • 什么是依赖注入和控制反转
    • 服务生命周期:Transient、Scoped、Singleton
    • 如何注册和使用服务
    • 接口与实现分离
  2. 中间件(Middleware)

    • 中间件的概念和作用
    • 请求管道的工作原理
    • 内置中间件的使用
    • 自定义中间件的编写
  3. 最佳实践

    • 使用接口提高可测试性
    • 合理选择服务生命周期
    • 注意中间件的注册顺序

实践成果

✅ 重构图书管理系统,使用依赖注入

✅ 创建自定义服务(BookService)

✅ 创建自定义中间件(RequestLoggingMiddleware)

✅ 理解了服务生命周期的区别

✅ 掌握了请求管道的工作原理


下周预告:第17章 将深入学习 MVC 模式,学习如何创建 Model(模型)、View(视图)和 Controller(控制器),构建完整的 Web 页面!

相关推荐
倚肆2 小时前
Kafka 生产者与消费者配置详解
java·分布式·后端·kafka
Honmaple2 小时前
Cherry Studio API完整参考手册(实操版)
后端
p***19942 小时前
SpringBoot项目中替换指定版本的tomcat
spring boot·后端·tomcat
古城小栈3 小时前
Rust中 引用类型 VS 裸指针
开发语言·后端·rust
Victor3563 小时前
MongoDB(12)如何启动和停止MongoDB服务?
后端
uzong3 小时前
老板说“哪里上不了?”——项目排期谈不拢?你缺的不是理由,是弹药
后端
Victor3563 小时前
MongoDB(13)如何配置MongoDB的存储路径?
后端
苍何12 小时前
即梦Seedance2.0海外火爆出圈,AI 视频的 DeepSeek 时刻来了!(附实测教程)
后端
苍何12 小时前
阿里卷麻了,千问 Qwen-Image-2.0 发布,超强文字渲染、信息图、PPT 轻松做(附实测提示词)
后端