Asp.Net Web Api如何在ActionFilter中捕获GET/POST请求日志
解决ASP.NET Web API ActionFilter同时捕获GET/POST请求内容的问题
我来帮你搞定这个日志统一记录的问题!核心点在于GET和POST请求的参数存储位置不一样:GET的参数在URL的QueryString里,而POST的在请求Body中,我们需要在ActionFilter里分别处理这两种情况。
完整的CustomLogAttribute实现
public class CustomLogAttribute : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { // 从Request.Properties中获取已授权的用户和AccessToken if (!actionContext.Request.Properties.TryGetValue("User", out var userObj) || userObj is not User user) { // 可以在这里记录用户未找到的警告日志 return; } var serviceTicket = actionContext.Request.Properties.TryGetValue("AccessToken", out var ticketObj) ? ticketObj.ToString() : string.Empty; string requestContent = string.Empty; var requestMethod = actionContext.Request.Method.Method.ToUpperInvariant(); if (requestMethod == "GET") { // 处理GET请求:解析QueryString参数 var queryParams = actionContext.Request.RequestUri.ParseQueryString(); requestContent = string.Join("&", queryParams.AllKeys.Select(key => $"{Uri.EscapeDataString(key)}={Uri.EscapeDataString(queryParams[key])}")); } else { // 处理POST/PUT/DELETE等带请求体的请求 try { // 异步读取请求体,避免潜在死锁 requestContent = actionContext.Request.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult(); // 关键:读取后重置流位置,确保后续控制器能正常获取请求体 if (actionContext.Request.Properties.TryGetValue("MS_HttpContext", out var contextObj) && contextObj is HttpContextBase httpContext) { httpContext.Request.InputStream.Seek(0, SeekOrigin.Begin); } } catch (Exception ex) { // 记录读取请求体失败的异常 Log.WriteError($"读取请求体失败: {ex.Message}"); } } // 记录开始日志(和你原来的逻辑一致) Log.Write(user.Id, "started", serviceTicket); // 记录详细请求日志 var methodName = actionContext.ActionDescriptor.ActionName; Log.WriteDetail(requestContent, user.Id, methodName, serviceTicket); base.OnActionExecuting(actionContext); } public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) { // 记录结束日志(放在Action执行后,和原逻辑匹配) if (actionExecutedContext.Request.Properties.TryGetValue("User", out var userObj) && userObj is User user) { var serviceTicket = actionExecutedContext.Request.Properties.TryGetValue("AccessToken", out var ticketObj) ? ticketObj.ToString() : string.Empty; Log.Write(user.Id, "finished", serviceTicket); } base.OnActionExecuted(actionExecutedContext); } }
关键细节说明
GET请求参数处理:
- 通过
RequestUri.ParseQueryString()解析URL中的查询参数,然后拼接成类似key1=value1&key2=value2的字符串。 - 使用
Uri.EscapeDataString()对参数值进行编码,避免特殊字符导致日志格式混乱。
- 通过
POST请求体处理:
- 读取请求体后必须重置
InputStream的位置到开头,否则后续的控制器方法(比如使用[FromBody]绑定参数)会读取不到内容,这是很多人容易忽略的坑! - 用
ConfigureAwait(false).GetAwaiter().GetResult()替代直接调用.Result,减少异步操作的死锁风险。
- 读取请求体后必须重置
日志时机拆分:
- 把
started日志放在OnActionExecuting(接口执行前),finished日志放在OnActionExecuted(接口执行后),完全匹配你原来每个接口里的日志顺序。
- 把
健壮性优化:
- 对
User和AccessToken的获取做了空值判断,避免空引用异常。 - 捕获读取请求体的异常,防止日志逻辑影响主业务流程。
- 对
使用方式
你可以选择两种方式应用这个Filter:
- 全局注册(所有接口都生效):在
WebApiConfig.cs中添加public static void Register(HttpConfiguration config) { config.Filters.Add(new CustomLogAttribute()); // 其他WebAPI配置... } - 局部应用(指定控制器/接口生效):在控制器或接口方法上添加特性
[CustomAuthorization] [CustomLog] public class ContentController : BaseApiController { // 你的接口方法... }
额外注意事项
- 如果有敏感参数(比如密码、手机号),记得在日志记录前做脱敏处理,不要直接写入日志。
- 对于大文件上传的POST请求,可以考虑跳过日志记录,避免占用过多日志存储空间。
- 其他HTTP方法(PUT/DELETE等)的参数处理逻辑已经包含在代码中,不需要额外修改。
内容的提问来源于stack exchange,提问作者enginocal
相关产品推荐
相关产品推荐

