依赖注入与中间件 - 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 页面!

相关推荐
IT_陈寒2 分钟前
SpringBoot性能飙升200%?这5个隐藏配置你必须知道!
前端·人工智能·后端
aiopencode1 小时前
使用 Ipa Guard 命令行版本将 IPA 混淆接入自动化流程
后端·ios
掘金者阿豪1 小时前
Kavita+cpolar 打造随身数字书房,让资源不再混乱,通勤 、出差都能随心读。
后端
心之语歌2 小时前
Spring Security api接口 认证放行
后端
用户8356290780512 小时前
Python 实现 PPT 转 HTML
后端·python
0xDevNull2 小时前
MySQL索引进阶用法
后端·mysql
舒一笑2 小时前
程序员效率神器:一文掌握 tmux(服务器开发必备工具)
运维·后端·程序员
UIUV3 小时前
Splitter学习笔记(含RAG相关流程与代码实践)
后端·langchain·llm
cipher3 小时前
HAPI + 设备指纹认证:打造更安全的远程编程体验
前端·后端·ai编程
雨中飘荡的记忆3 小时前
保证金系统入门到实战
java·后端