利用过滤器特性优雅实现接口操作日志记录
在Web开发中,记录用户操作日志是常见需求。传统方式需要在每个接口中重复编写日志代码,不仅繁琐且容易遗漏。本文将介绍如何通过ASP.NET Core过滤器特性,实现零侵入的操作日志记录方案。
一、传统方式 vs 过滤器方案
传统方式痛点:
public async Task<IActionResult> AddUser([FromBody] UserDto dto)
{
// 业务逻辑前
LogStart(dto);
try {
// 业务逻辑
LogSuccess(result);
} catch {
LogError(ex);
}
}
过滤器方案优势:
-
✅ 业务与日志分离
-
✅ 统一异常处理
-
✅ 避免重复代码
-
✅ 支持动态日志描述
二、实现步骤
1. 创建日志过滤器
`using MySite.Entities;
using MySite.Exceptions;
using MySite.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using SqlSugar;
using System.Security.Claims;
namespace MySite.Filters
{
/// <summary>
/// 日志记录过滤器
/// @author : chenjingzhi
/// @date : 2025/7/3 18:41:18
/// @desc :
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public class OperationLogAttribute : Attribute, IFilterFactory
{
public string _operationDetail { get; }
public bool IsReusable => false; // 确保每次请求都创建新实例
public OperationLogAttribute(string operationDetail)
{
_operationDetail = operationDetail;
}
// 通过 DI 容器创建 Filter 实例
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
// 从 DI 容器中获取 OperationLogFilter 实例
var filter = serviceProvider.GetRequiredService<OperationLogFilter>();
// 动态设置操作信息
filter.SetOperationInfo(_operationDetail);
return filter;
}
}
/// <summary>
///
/// </summary>
public class OperationLogFilter : IAsyncActionFilter
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ISqlSugarClient _sqlSugarClient;
private string _operationDetail;
/// <summary>
///
/// </summary>
/// <param name="httpContextAccessor"></param>
/// <param name="sqlSugarClient"></param>
/// <exception cref="ArgumentNullException"></exception>
public OperationLogFilter(IHttpContextAccessor httpContextAccessor, ISqlSugarClient sqlSugarClient)
{
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
_sqlSugarClient = sqlSugarClient;
}
// 动态设置操作信息
public void SetOperationInfo(string operationDetail)
{
_operationDetail = operationDetail;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// 获取方法信息
var methodName = context.ActionDescriptor.DisplayName; // 方法名
var className = context.Controller.GetType().Name; // 类名
var parameters = context.ActionArguments; // 方法参数
var startTime = DateTime.Now; // 开始时间
// 模拟获取当前用户(实际可以从上下文或认证信息中获取)
var currentUser = GetCurrentUserId(_httpContextAccessor);
var operateSystem = HttpContextClient.GetClientOperatingSystem(_httpContextAccessor.HttpContext) + "|" +
HttpContextClient.GetClientBrowser(_httpContextAccessor.HttpContext);
try
{
var resultContext = await next();
// 获取操作结果
var operationResult = "成功";
var outputParam = string.Empty;
if (resultContext.Exception != null)
{
// 如果发生异常,记录失败日志
operationResult = "失败";
if (resultContext.Exception is ResultMessageException resultMessageException)
{
// 如果是 ResultMessageException,记录异常消息
outputParam = resultMessageException.Message;
}
else
{
// 如果是其他异常,记录异常信息
outputParam = resultContext.Exception.Message;
}
}
else if (resultContext.Result != null)
{
if (resultContext.Result is ObjectResult objectResult)
{
// 如果是 ObjectResult,序列化返回值
outputParam = JsonConvert.SerializeObject(objectResult.Value);
}
else if (resultContext.Result is StatusCodeResult statusCodeResult)
{
// 如果是状态码结果,记录状态码
outputParam = $"StatusCode: {statusCodeResult.StatusCode}";
if (statusCodeResult.StatusCode >= 400)
{
operationResult = "失败";
}
}
}
// 根据方法名生成日志信息
await GenerateLogMessage(methodName, className, _operationDetail, currentUser, operateSystem, startTime, parameters, operationResult, outputParam);
}
catch (ResultMessageException ex)
{
// 捕获 ResultMessageException 并记录失败日志
await GenerateLogMessage(methodName, className, _operationDetail, currentUser, operateSystem, startTime, parameters, "失败", ex.Message);
throw; // 重新抛出异常,确保全局异常处理仍然生效
}
catch (Exception ex)
{
// 记录其他异常日志
await GenerateLogMessage(methodName, className, _operationDetail, currentUser, operateSystem, startTime, parameters, "失败", ex.Message);
throw; // 重新抛出异常,确保全局异常处理仍然生效
}
}
/// <summary>
/// 模拟获取当前用户ID
/// </summary>
private string GetCurrentUserId(IHttpContextAccessor httpContextAccessor)
{
// 实际可以从 HttpContext.User 中获取用户信息
var userIdClaim = httpContextAccessor.HttpContext?.User?.Claims
.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
return userIdClaim?.Value ?? "Anonymous";
}
/// <summary>
/// 生成日志信息 此处写入数据库,也可写日志文件或其他存储
/// </summary>
/// <param name="methodName">http方法名</param>
/// <param name="className">类名</param>
/// <param name="operationDetail">操作详情</param>
/// <param name="currentUser">当前用户</param>
/// <param name="operateSystem">操作系统</param>
/// <param name="startTime">开始时间</param>
/// <param name="parameters">入参</param>
/// <param name="operationResult">操作结果</param>
/// <param name="outputParam">出参</param>
/// <returns></returns>
private async Task GenerateLogMessage(string methodName, string className, string operationDetail, string currentUser,
string operateSystem, DateTime startTime, IDictionary<string, object> parameters, string operationResult, string outputParam)
{
try
{
var operationLog = new Tb_Operation_Logs
{
Id = Guid.NewGuid().ToString(),
UserId = currentUser,
OperationTime = startTime,
OperationType = 1,
OperationDetail = operationDetail,
OperationResult = operationResult,
Method = methodName,
FailureReason = operationResult == "失败" ? outputParam : string.Empty,
OperationSystem = operateSystem,
IpAddress = _httpContextAccessor.HttpContext?.Connection?.RemoteIpAddress?.ToString(),
InputParam = JsonConvert.SerializeObject(parameters),
OutputParam = operationResult == "成功" ? outputParam : string.Empty
};
await _sqlSugarClient.Insertable(operationLog).ExecuteCommandAsync();
}
catch (Exception ex)
{
Console.WriteLine($"系统错误,写入操作日志错误: {ex.Message}");
}
}
}
}
2. 服务注册(Program.cs)
builder.Services
.AddHttpContextAccessor()
.AddTransient<OperationLogFilter>();
3. 控制器使用示例
[ApiController]
[Route("api/users")]
public class UserController : ControllerBase
{
[HttpPost]
[OperationLog("创建新用户")]
public IActionResult CreateUser([FromBody] UserCreateDto dto)
{
// 纯净的业务逻辑
var newUser = _userService.Create(dto);
return CreatedAtAction(nameof(GetUser), new { id = newUser.Id }, newUser);
}
[HttpDelete("{id}")]
[OperationLog("删除用户账户")]
public IActionResult DeleteUser(string id)
{
_userService.Delete(id);
return NoContent();
}
}
三、方案亮点
1. 动态日志描述
支持在特性中直接定义可读性强的操作描述:
[OperationLog("审批报销单")]
public IActionResult ApproveExpense(...)
2. 智能异常处理
sequenceDiagram
participant Client
participant Filter
participant Controller
participant Database
Client->>Filter: 请求接口
Filter->>Controller: 执行前置逻辑
Controller-->>Filter: 返回结果/异常
alt 成功
Filter->>Database: 记录成功日志
else 失败
Filter->>Database: 记录错误日志
Filter-->>Client: 返回错误信息
end
3. 完整上下文捕获
日志包含的关键信息:
{
"UserId": "u12345",
"Operation": "修改订单状态",
"Params": {"orderId":1001, "status":"SHIPPED"},
"System": "Windows|Chrome",
"IP": "192.168.1.100",
"Result": "成功",
"Duration": "120ms"
}
四、高级优化技巧
1. 敏感数据过滤
private string SanitizeInput(object input)
{
var json = JsonConvert.SerializeObject(input);
return Regex.Replace(json, @"(""password\"":\s*"")([^""]+)","$1***");
}
2. 性能优化
// 异步批量写入
private static readonly ConcurrentQueue<OperationLog> _logQueue = new();
// 后台任务定期处理
builder.Services.AddHostedService<LogBackgroundService>();
3. 动态操作描述
// 支持占位符
[OperationLog("更新#{id}订单")]
// 在过滤器中解析
var detail = _operationDetail.Replace("#{id}", context.ActionArguments["id"].ToString());
五、适用场景对比
场景 | 过滤器方案 | AOP方案 | 中间件方案 |
---|---|---|---|
需要方法参数 | ✅ | ✅ | ❌ |
需要返回值 | ✅ | ✅ | ❌ |
需要具体操作描述 | ✅ | ❌ | ❌ |
全局日志记录 | ✅ | ✅ | ✅ |
与业务逻辑解耦 | ✅ | ✅ | ✅ |
六、总结
通过过滤器实现操作日志记录具有以下核心优势:
-
关注点分离 - 业务代码保持纯净
-
声明式编程 - 通过特性配置日志行为
-
上下文完备 - 完整获取方法参数和返回值
-
异常安全 - 内置统一的错误处理机制
-
灵活扩展 - 支持动态日志模板和敏感数据处理
最佳实践建议:对于管理后台、财务系统等高安全要求场景,建议组合使用过滤器日志+数据库审计功能,实现操作留痕的双重保障。
// 最终的优雅使用方式
[OperationLog("${userName}修改了部门设置")]
public IActionResult UpdateDepartment(DepartmentUpdateDto dto)
{
// 只需关注核心业务逻辑
_deptService.Update(dto);
return Ok();
}