基于EF Core、Swagger与C#的HTTP请求拦截及日志管理需求
实现HTTP请求拦截记录与定时清理日志
一、创建请求日志实体与EF Core配置
首先定义请求日志实体类,包含核心记录字段:
public class RequestLog { public int Id { get; set; } public string Path { get; set; } public string Method { get; set; } public string QueryString { get; set; } public string RequestBody { get; set; } public DateTime CreatedAt { get; set; } public string ClientIp { get; set; } }
在DbContext中配置日志表的映射:
public class AppDbContext : DbContext { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } public DbSet<RequestLog> RequestLogs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 给CreatedAt字段加索引,提升清理时的查询效率 modelBuilder.Entity<RequestLog>() .HasIndex(l => l.CreatedAt); } }
执行EF迁移命令生成数据库表:
Add-Migration AddRequestLogsTable Update-Database
二、编写请求拦截中间件(解决RequestDelegate存储失败问题)
之前用RequestDelegate无法存库的核心原因是中间件生命周期与DbContext不匹配(中间件是单例,DbContext是作用域服务),需通过IServiceScopeFactory创建作用域获取DbContext:
public class RequestLoggingMiddleware { private readonly RequestDelegate _next; private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger<RequestLoggingMiddleware> _logger; public RequestLoggingMiddleware(RequestDelegate next, IServiceScopeFactory scopeFactory, ILogger<RequestLoggingMiddleware> logger) { _next = next; _scopeFactory = scopeFactory; _logger = logger; } public async Task InvokeAsync(HttpContext context) { // 排除Swagger相关请求,避免日志冗余 var path = context.Request.Path.ToString(); if (path.StartsWith("/swagger") || path.StartsWith("/swagger-ui")) { await _next(context); return; } // 启用请求体缓冲,保证读取后后续中间件仍能使用 context.Request.EnableBuffering(); var requestBody = await new StreamReader(context.Request.Body).ReadToEndAsync(); context.Request.Body.Position = 0; // 创建作用域获取DbContext using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); var log = new RequestLog { Path = context.Request.Path, Method = context.Request.Method, QueryString = context.Request.QueryString.ToString(), RequestBody = requestBody, CreatedAt = DateTime.UtcNow, ClientIp = context.Connection.RemoteIpAddress?.ToString() }; try { dbContext.RequestLogs.Add(log); await dbContext.SaveChangesAsync(); } catch (Exception ex) { _logger.LogError(ex, "保存请求日志失败"); // 记录失败不中断主请求流程 } await _next(context); } }
在Program.cs中注册中间件(放在Swagger中间件之后,避免记录Swagger请求):
var builder = WebApplication.CreateBuilder(args); // 注册DbContext builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); // 注册日志服务 builder.Services.AddLogging(); // 注册Swagger builder.Services.AddSwaggerGen(); var app = builder.Build(); // 注册请求日志中间件 app.UseMiddleware<RequestLoggingMiddleware>(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
三、实现7天一次的日志清理定时任务
使用.NET自带的BackgroundService实现定时清理逻辑:
public class LogCleanupService : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger<LogCleanupService> _logger; private readonly TimeSpan _cleanupInterval = TimeSpan.FromDays(7); public LogCleanupService(IServiceScopeFactory scopeFactory, ILogger<LogCleanupService> logger) { _scopeFactory = scopeFactory; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("日志清理服务已启动"); while (!stoppingToken.IsCancellationRequested) { try { using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); // 删除7天前的日志 var cutoffDate = DateTime.UtcNow.AddDays(-7); var oldLogs = await dbContext.RequestLogs .Where(l => l.CreatedAt < cutoffDate) .ToListAsync(stoppingToken); dbContext.RequestLogs.RemoveRange(oldLogs); await dbContext.SaveChangesAsync(stoppingToken); _logger.LogInformation("已清理{Count}条过期请求日志", oldLogs.Count); } catch (Exception ex) { _logger.LogError(ex, "清理过期日志失败"); } // 等待7天后再次执行 await Task.Delay(_cleanupInterval, stoppingToken); } _logger.LogInformation("日志清理服务已停止"); } }
在Program.cs中注册后台服务:
builder.Services.AddHostedService<LogCleanupService>();
四、关键注意事项
- 请求体读取:必须调用
context.Request.EnableBuffering(),否则流只能读取一次;读取后要重置流位置context.Request.Body.Position = 0,保证后续中间件正常获取请求体。 - DbContext生命周期:中间件是单例,不能直接注入DbContext,必须通过
IServiceScopeFactory创建作用域,避免DbContext被长时间持有引发并发问题。 - 异常隔离:日志存储和清理的异常要单独捕获,不能影响主业务请求的执行。
- Swagger过滤:根据实际需求决定是否排除Swagger请求,减少无效日志。
内容的提问来源于stack exchange,提问作者Jacob Modiba
相关产品推荐
相关产品推荐

