第19章 抽离“EntityFrameworkCore”中间件实例的依赖注入

'# 第19章 抽离“EntityFrameworkCore”中间件实例的依赖注入

一、背景与问题

在ASP.NET Core应用中,Entity Framework Core(EF Core)作为核心的ORM框架,其与中间件(Middleware)的协作是系统架构中的关键环节。中间件通常用于处理HTTP请求的生命周期,例如日志记录、身份验证、请求拦截等。然而,当需要在中间件中使用EF Core时,会面临几个核心问题:

  1. 生命周期管理:EF Core的DbContext实例通常需要Scoped生命周期,而中间件默认是Singleton生命周期,直接注入可能导致资源泄露或并发问题。
  2. 依赖注入的可测试性:直接在中间件中硬编码EF Core的实例,会降低测试的灵活性和可维护性。
  3. 性能瓶颈:如果中间件频繁创建或重复使用DbContext实例,可能引发性能问题。

例如,一个日志记录中间件需要将请求信息保存到数据库时,若直接在中间件中创建DbContext实例,可能会导致以下问题:

  • 多线程环境下出现并发访问冲突
  • 未正确关闭数据库连接
  • 依赖注入未正确配置导致的运行时异常

二、基本原理

EF Core的依赖注入机制基于ASP.NET Core的内置服务容器。要正确使用EF Core的中间件,需要理解以下核心概念:

1. 服务生命周期

  • Singleton:整个应用生命周期内只创建一次
  • Scoped:每个请求创建一次(默认)
  • Transient:每次请求都创建新实例

2. 中间件生命周期

中间件的实例默认是Singleton,但其内部方法(Invoke/InvokeAsync)可以访问Scoped服务。

3. 依赖注入的实现

通过在Startup.cs(或Program.cs)中注册EF Core服务,利用AddDbContext方法定义服务生命周期。

三、环境准备

1. 项目结构

MyApp/
├── Program.cs
├── Startup.cs
├── Services/
│   └── LoggingService.cs
├── Middlewares/
│   └── LoggingMiddleware.cs
├── Models/
│   └── LogEntry.cs
└── Data/
    └── ApplicationDbContext.cs

2. 安装依赖

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

四、核心实现

1. 注册EF Core服务

// Startup.cs 或 Program.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    
    services.AddTransient<ILoggingService, LoggingService>();
    
    services.AddHttpContextAccessor();
}

关键点:

  • 使用AddDbContext定义DbContext的服务生命周期
  • 注册日志服务作为Transient服务
  • 添加IHttpContextAccessor以获取当前请求上下文

2. 中间件中注入EF Core

// Middlewares/LoggingMiddleware.cs
public class LoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILoggingService _loggingService;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public LoggingMiddleware(
        RequestDelegate next,
        ILoggingService loggingService,
        IHttpContextAccessor httpContextAccessor)
    {
        _next = next;
        _loggingService = loggingService;
        _httpContextAccessor = httpContextAccessor;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var logEntry = new LogEntry
        {
            Path = context.Request.Path,
            Method = context.Request.Method,
            Timestamp = DateTime.UtcNow
        };

        await _loggingService.LogAsync(logEntry); // 使用注入的服务

        await _next(context);
    }
}

关键点:

  • 中间件构造函数注入了ILoggingService(Transient)和IHttpContextAccessor
  • 通过IHttpContextAccessor获取当前请求上下文
  • 调用日志服务完成数据库操作

3. 日志服务实现

// Services/LoggingService.cs
public interface ILoggingService
{
    Task LogAsync(LogEntry logEntry);
}

public class LoggingService : ILoggingService
{
    private readonly ApplicationDbContext _context;

    public LoggingService(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task LogAsync(LogEntry logEntry)
    {
        _context.LogEntries.Add(logEntry);
        await _context.SaveChangesAsync();
    }
}

关键点:

  • LoggingService依赖ApplicationDbContext(Scoped)
  • 在中间件中通过依赖注入获取实例

五、完整案例

1. 完整案例:日志中间件集成

1. 数据库上下文

// Data/ApplicationDbContext.cs
public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    public DbSet<LogEntry> LogEntries { get; set; }
}

2. 日志实体

// Models/LogEntry.cs
public class LogEntry
{
    public int Id { get; set; }
    public string Path { get; set; }
    public string Method { get; set; }
    public DateTime Timestamp { get; set; }
}

3. 中间件注册

// Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseMiddleware<LoggingMiddleware>();
    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

4. 测试API

[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return Ok("Test");
    }
}

5. 配置文件

// appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyAppDb; Trusted_Connection=True;"
  }
}

六、源码解析

1. 中间件生命周期管理

在LoggingMiddleware的构造函数中,ILoggingService被注入为Transient,而ApplicationDbContext在LoggingService中被注入为Scoped。这种组合确保了:

  • 每个请求都会创建新的LoggingService实例
  • LoggingService内部使用Scoped的ApplicationDbContext实例
  • 中间件的InvokeAsync方法可以安全地调用日志服务

2. 异常处理机制

public async Task InvokeAsync(HttpContext context)
{
    try
    {
        var logEntry = new LogEntry
        {
            Path = context.Request.Path,
            Method = context.Request.Method,
            Timestamp = DateTime.UtcNow
        };

        await _loggingService.LogAsync(logEntry);
        await _next(context);
    }
    catch (Exception ex)
    {
        // 记录异常日志
        await _loggingService.LogAsync(new LogEntry
        {
            Path = "Error",
            Method = "Error",
            Timestamp = DateTime.UtcNow,
            ErrorMessage = ex.Message
        });
        throw;
    }
}

关键点:

  • 使用try-catch块捕获异常
  • 在日志服务中记录异常信息
  • 重新抛出异常确保中间件的正常流程

七、进阶使用

1. 使用IOptions获取配置

public class LoggingMiddleware
{
    private readonly IOptions<LoggingOptions> _options;

    public LoggingMiddleware(IOptions<LoggingOptions> options)
    {
        _options = options;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var logEntry = new LogEntry
        {
            Path = context.Request.Path,
            Method = context.Request.Method,
            Timestamp = DateTime.UtcNow,
            LogLevel = _options.Value.LogLevel
        };

        await _loggingService.LogAsync(logEntry);
        await _next(context);
    }
}

2. 使用工厂模式创建DbContext

public class DbContextFactory : IDbContextFactory<ApplicationDbContext>
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public DbContextFactory(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public ApplicationDbContext Create()
    {
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseSqlServer("YourConnectionString")
            .Options;

        return new ApplicationDbContext(options);
    }
}

八、性能与工程实践

1. 性能优化策略

优化措施说明
使用异步操作所有数据库操作应使用async/await
避免N+1查询使用Include或ThenInclude进行显式加载
缓存常用查询对频繁访问的数据使用内存缓存
禁用自动更改跟踪在不需要时禁用ChangeTracker
使用批处理操作对大量数据使用SaveChangesAsync批量提交

2. 安全风险分析

风险点解决方案
SQL注入使用参数化查询,避免直接拼接SQL
数据泄露使用敏感数据加密存储,限制数据库访问权限
注入未授权服务严格控制中间件中注入的服务范围
未处理的异常增加全局异常处理程序(UseExceptionHandler)

3. 异常处理机制

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseExceptionHandler("/error");
    app.UseMiddleware<LoggingMiddleware>();
    app.UseRouting();
}

九、常见问题与踩坑

1. 常见错误示例

错误代码:

public class LoggingMiddleware
{
    private readonly ApplicationDbContext _context;

    public LoggingMiddleware()
    {
        _context = new ApplicationDbContext();
    }

    public async Task Invoke(HttpContext context)
    {
        var logEntry = new LogEntry
        {
            Path = context.Request.Path,
            Method = context.Request.Method,
            Timestamp = DateTime.UtcNow
        };

        _context.LogEntries.Add(logEntry);
        await _context.SaveChangesAsync();
    }
}

问题分析:

  • 直接实例化DbContext导致生命周期管理混乱
  • 中间件是Singleton,但DbContext是Scoped,导致上下文状态不一致
  • 未处理异常可能导致数据库连接泄露

2. 解决方案

正确代码:

public class LoggingMiddleware
{
    private readonly ILoggingService _loggingService;

    public LoggingMiddleware(ILoggingService loggingService)
    {
        _loggingService = loggingService;
    }

    public async Task Invoke(HttpContext context)
    {
        var logEntry = new LogEntry
        {
            Path = context.Request.Path,
            Method = context.Request.Method,
            Timestamp = DateTime.UtcNow
        };

        await _loggingService.LogAsync(logEntry);
    }
}

3. 典型问题场景

场景问题解决方案
中间件中直接创建DbContext上下文状态不一致使用依赖注入获取实例
没有处理异常日志丢失增加try-catch块
未配置IHttpContextAccessor无法获取当前请求在Startup.cs中注册服务

十、最佳实践

1. 推荐方案

  1. 依赖注入规范

    • 所有中间件必须通过构造函数注入依赖项
    • 严格遵循服务生命周期
    • 避免在中间件中直接创建EF Core实例
  2. 日志处理规范

    • 使用单独的日志服务处理数据库操作
    • 增加异常捕获和重试机制
    • 使用日志级别控制日志输出
  3. 性能优化方案

    • 对频繁访问的数据进行缓存
    • 使用批处理操作减少数据库调用
    • 禁用不必要的变更跟踪

2. 推荐代码结构

// Middlewares/
│── LoggingMiddleware.cs
│── AuthMiddleware.cs
│── ExceptionMiddleware.cs

// Services/
│── LoggingService.cs
│── AuthService.cs
│── CacheService.cs

// Data/
│── ApplicationDbContext.cs
│── DbFactory.cs
│── RepositoryBase.cs

十一、总结

本文深入探讨了在ASP.NET Core中正确使用Entity Framework Core的中间件依赖注入的实现原理与实践方法。通过三个代码示例和一个完整案例,展示了如何安全地在中间件中使用EF Core服务。重点分析了生命周期管理、异常处理、性能优化等关键问题,并提供了常见错误的解决方案。

在实际开发中,这种模式特别适用于需要在中间件中进行日志记录、安全检查、请求拦截等场景。但需要注意,不建议在中间件中直接创建EF Core实例,也不建议在Singleton作用域中使用Scoped服务。通过合理使用依赖注入,可以显著提高代码的可维护性、可测试性和性能表现。

最后,建议开发者在实施时结合具体业务场景,选择合适的依赖注入方式,并通过单元测试和性能测试验证实现的正确性。

最后修改于:2026年09月22日 20:25

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日