在 ASP.NET Core 项目中,过滤器(Filter)是实现横切关注点(Cross-Cutting Concerns)的核心机制。不同于中间件(Middleware)在请求管道全局生效,过滤器工作在 Action 执行的前后,能够精细地控制和捕获方法级别的执行上下文。
本文将基于 SwitchData.Api 项目中的
ApiDbLoggingActionFilter
实现,带你深入理解如何封装一个功能强大的数据库操作日志过滤器,并掌握其中涉及的高级技术点:异步日志写入、匿名对象处理、上下文状态捕获等。
1. 过滤器基础:为什么选择 IAsyncActionFilter?
在 ASP.NET Core 中,过滤器有多种类型,其中
IAsyncActionFilter 是最常用的 Action
级别过滤器。它的核心方法 OnActionExecutionAsync
接收两个关键参数: - ActionExecutingContext:Action
执行前的上下文,可获取入参、Controller 信息、请求头。 -
ActionExecutionDelegate:一个委托,调用它会执行后续的
Action 及其他过滤器。
我们的策略是在 next() 前捕获入参,在
next()
后捕获出参和异常,实现完整的请求链路日志记录。
2. 日志过滤器核心实现
以下是从项目中提取的核心代码结构,展示了一个完整的异步日志过滤器设计思路。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace SwitchData.Api.Filters
{
/// <summary>
/// API 数据库操作日志过滤器
/// </summary>
public class ApiDbLoggingActionFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// 1. 前置检查:判断是否需要跳过日志记录
if (!ShouldLog(context))
{
await next();
return;
}
// 2. 捕获入参
var inputParameters = CaptureInputParameters(context.ActionArguments);
// 3. 执行 Action(进入管道)
var executedContext = await next();
// 4. 捕获出参和异常
var outputResult = await CaptureOutputResultAsync(executedContext);
// 5. 异步写入日志(fire-and-forget,不阻塞响应)
_ = WriteLogAsync(context, inputParameters, outputResult, executedContext);
}
private bool ShouldLog(ActionExecutingContext context)
{
// 检查是否存在跳过日志的权限标识
var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
if (controllerActionDescriptor != null)
{
var methodInfo = controllerActionDescriptor.MethodInfo;
var attrs = methodInfo.GetCustomAttributes(typeof(PermissionAttribute), false);
if (attrs.Length > 0)
{
var perm = attrs[0] as PermissionAttribute;
// 如果方法标注了白名单权限,则跳过日志
if (perm?.Permission?.Contains("White.Default") ?? false)
return false;
}
}
return true;
}
}
}
3. 关键技术点解析
3.1 敏感参数脱敏:入参捕获与过滤
在捕获入参时,密码、密钥、Token 等敏感信息绝不能原样入库。我们通过反射遍历 Action 参数,识别敏感字段并进行掩码处理:
private string CaptureInputParameters(IDictionary<string, object> actionArguments)
{
if (actionArguments == null || actionArguments.Count == 0)
return "{}";
try
{
var safeArguments = new Dictionary<string, object>();
// 定义敏感关键字列表
var sensitiveKeys = new[] { "password", "pwd", "token", "secret", "key" };
foreach (var arg in actionArguments)
{
// 包含敏感关键字的参数直接替换为 ***
if (sensitiveKeys.Any(s => arg.Key.Contains(s, System.StringComparison.OrdinalIgnoreCase)))
{
safeArguments[arg.Key] = "***";
}
else
{
safeArguments[arg.Key] = arg.Value;
}
}
return JsonHelper.Serialize(safeArguments);
}
catch (System.Exception ex)
{
return $"入参序列化失败: {ex.Message}";
}
}
设计亮点:使用
StringComparison.OrdinalIgnoreCase 忽略大小写匹配,确保
Password、password、PWD
等变体都能被正确识别。
3.2 智能出参处理:多类型结果捕获
Action
的返回值类型是多样的——ObjectResult、JsonResult、ContentResult、StatusCodeResult
等都可能出现。我们需要根据具体类型进行差异化处理:
private async Task<string> CaptureOutputResultAsync(ActionExecutedContext executedContext)
{
return await Task.Run(() =>
{
// 1. 异常情况:优先记录异常信息
if (executedContext.Exception != null)
return $"异常信息: {executedContext.Exception.Message}";
if (executedContext.Result == null)
return "{}";
try
{
string resultJson;
// 2. 根据 Result 类型分别处理
switch (executedContext.Result)
{
case ObjectResult objectResult:
resultJson = objectResult.Value != null
? JsonHelper.Serialize(TruncateAnonymousType(objectResult.Value))
: "null";
break;
case JsonResult jsonResult:
resultJson = jsonResult.Value != null
? JsonHelper.Serialize(TruncateAnonymousType(jsonResult.Value))
: "null";
break;
case ContentResult contentResult:
resultJson = $"内容: {contentResult.Content}";
break;
case StatusCodeResult statusCodeResult:
resultJson = $"状态码: {statusCodeResult.StatusCode}";
break;
case EmptyResult:
resultJson = "空结果";
break;
default:
resultJson = $"结果类型: {executedContext.Result.GetType().Name}";
break;
}
return resultJson;
}
catch (System.Exception ex)
{
return $"出参序列化失败: {ex.Message}";
}
});
}
设计亮点:使用 Task.Run
将出参序列化工作放到线程池执行,避免阻塞主请求线程。对于大数据量的序列化场景,这一优化尤为重要。
### 3.3 匿名对象截断:ExpandoObject 动态处理
最具挑战性的部分是:Controller
的出参可能是匿名类型(匿名类),我们无法预先知道其结构,且序列化后的数据可能非常庞大。这里使用了
ExpandoObject + 反射进行动态截断:
/// <summary>
/// 截取匿名类型字符串属性长度(防止日志过长)
/// </summary>
private object TruncateAnonymousType(object obj, int maxLength = 1000, string suffix = "...(已截断)")
{
if (obj == null) return null;
System.Type type = obj.GetType();
// 创建动态对象(ExpandoObject 可以在运行时动态添加属性)
dynamic result = new ExpandoObject();
var resultDict = (IDictionary<string, object>)result;
// 通过反射遍历所有属性
foreach (System.Reflection.PropertyInfo prop in type.GetProperties())
{
if (prop.CanRead)
{
object value = prop.GetValue(obj);
resultDict[prop.Name] = TruncateValue(value, maxLength, suffix);
}
}
return result;
}
private object TruncateValue(object value, int maxLength, string suffix)
{
if (value == null) return null;
System.Type valueType = value.GetType();
// 处理字符串:超长截断
if (valueType == typeof(string))
{
string str = (string)value;
if (str.Length <= maxLength)
return str;
int truncateLength = maxLength - suffix.Length;
return truncateLength > 0 ? str.Substring(0, truncateLength) + suffix : suffix;
}
// 处理嵌套匿名类型:递归截断
if (valueType.Name.Contains("AnonymousType"))
{
return TruncateAnonymousType(value, maxLength, suffix);
}
// 处理列表/数组
if (value is System.Collections.IEnumerable enumerable && !(value is string))
{
var resultList = new List<object>();
foreach (var item in enumerable)
{
resultList.Add(TruncateValue(item, maxLength, suffix));
}
return resultList.ToArray();
}
return value;
}
设计亮点: 1. ExpandoObject:这是 .NET 中处理动态对象的利器,无需定义实体类即可构建键值对结构,非常适合日志场景的灵活需求。 2. 递归处理:嵌套类型和集合都能被正确截断,确保日志的可控体积。 3. 智能截断:保留前 1000 字符 + 截断标记,既保留了关键信息,又防止日志膨胀。
3.4 Fire-and-Forget:异步日志写入
日志写入是典型的 I/O 密集操作(数据库写入),绝不能阻塞主请求的响应。我们使用以下模式实现真正的异步解耦:
private async Task WriteLogAsync(
ActionExecutingContext context,
string inputParams,
string outputParams,
ActionExecutedContext executedContext)
{
try
{
await Task.Run(() =>
{
var messageBuilder = new StringBuilder();
// 根据执行结果构建日志消息
if (executedContext.Exception != null)
{
messageBuilder.Append($"执行失败 - 异常: {executedContext.Exception.Message}");
}
else
{
// 从统一响应中提取业务消息
if (executedContext.Result is ObjectResult { Value: JsonResultExtension obj })
messageBuilder.Append(obj.message ?? "执行完成");
else if (executedContext.Result is JsonResult { Value: JsonResultExtension json })
messageBuilder.Append(json.message ?? "执行完成");
else
messageBuilder.Append("执行完成");
}
// 调用底层日志服务入库
DbLogHelper.AddApiLog(
url: context.HttpContext.Request.Path,
controller: context.Controller.GetType().Name,
action: context.ActionDescriptor.DisplayName,
userid: context.HttpContext.Request.Headers["user-id"].ToString(),
ip: context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "",
request: inputParams,
response: outputParams,
message: messageBuilder.ToString(),
remark: string.Empty
);
}).ConfigureAwait(false);
}
catch (System.Exception ex)
{
// 日志失败不影响业务,仅输出到控制台
Console.WriteLine($"记录业务操作日志失败: {ex.Message}");
}
}
设计亮点: 1.
ConfigureAwait(false):避免在异步日志写入完成后回到原始同步上下文,减少线程池压力。
2.
异常兜底:日志服务的异常被捕获后仅输出到控制台,绝不会影响业务接口的正常返回。
3.
统一响应解析:巧妙地利用模式匹配提取业务响应中的消息字段,实现了日志内容与业务语义的关联。
4. 过滤器注册与全局应用
要让过滤器全局生效,需要在 Program.cs 中进行注册:
// Program.cs
builder.Services.AddControllers(options =>
{
// 注册全局 Action Filter
options.Filters.Add<ApiDbLoggingActionFilter>();
});
如果只需要在特定 Controller 或 Action 上应用,可以直接标注特性:
// Controller 级别(整个控制器的所有方法都记录)
[ApiDbLoggingActionFilter]
public class WorkController : ApiBaseController
{
// ...
}
// Action 级别(仅单个方法记录)
[ApiDbLoggingActionFilter]
public IActionResult AddWork([FromBody] AddRequest request)
{
// ...
}
5. 设计模式总结
通过这个日志过滤器的实现,我们可以总结出 ASP.NET Core 过滤器设计的几条最佳实践:
5.1 职责单一原则
过滤器只做日志相关的事,不混入业务逻辑。入参捕获、出参处理、日志写入各司其职。
5.2 性能优先原则
日志写入使用异步 fire-and-forget 模式,序列化使用线程池,避免任何可能阻塞主请求的同步操作。
5.3 安全优先原则
敏感参数脱敏、异常信息安全输出,确保日志系统不会成为信息泄露的源头。
5.4 可扩展原则
通过 ShouldLog
方法预留了灵活的跳过机制,可以根据权限标识、请求路径、用户角色等条件动态控制日志记录范围。
6. 总结
ASP.NET Core 的过滤器机制为实现横切关注点提供了优雅的解决方案。通过
IAsyncActionFilter,我们能够: - 在 Action
执行前后无侵入地注入日志逻辑 -
通过反射和动态类型处理未知的数据结构 -
利用异步编程确保日志操作不影响业务性能
这种设计模式不仅适用于操作日志场景,同样可以推广到审计追踪、性能监控、异常捕获等其他需要在方法级别介入的需求中。掌握了这个模式,你就能在 ASP.NET Core 项目中灵活应对各种横切关注点的开发挑战。