一、过滤器简介
ASP.NET Core中的过滤器是一种组件,它可以在请求处理管道中的特定阶段运行代码。过滤器有多种类型,包括授权过滤器、资源过滤器、动作过滤器、异常过滤器和结果过滤器。本文中使用的是动作过滤器(Action Filter
),它在动作方法执行前后执行,可以用来记录请求和响应信息。
二、自定义GlobalActionFilter类
1. 类定义
/// <summary>
/// Action过滤器
/// </summary>
public class GlobalActionFilter : IActionFilter, IOrderedFilter
{
/// <summary>
/// 构造
/// </summary>
/// <param name="logger"></param>
public GlobalActionFilter(SugarDbContext dbContext, ILogger<GlobalActionFilter> logger)
{
this.dbContext = dbContext;
this.logger = logger;
}
private readonly SugarDbContext dbContext;
private readonly ILogger<GlobalActionFilter> logger;
/// <summary>
/// 过滤器执行顺序
/// </summary>
public int Order => 2; // 设置执行顺序
private (string Method, string Path, string ClientIp) GetRequestInfo(HttpContext context)
{
string method = context.Request.Method;
string serverIp = context.Connection.LocalIpAddress.GetFormattedIpAddress();
string serverBaseUrl = $"http://{serverIp}:{context.Connection.LocalPort}";
string pathSmall = context.Request.Path.ToString();
string path = serverBaseUrl + pathSmall;
string clientIp = context.Connection.RemoteIpAddress.GetFormattedIpAddress();
return (method, path, clientIp);
}
private string GetInParam(ActionExecutingContext context)
{
return context.ActionArguments.Values.FirstOrDefault()?.ToJson() ?? "Null";
}
private string GetOutParam(ActionExecutedContext context)
{
JsonResult? response = context.Result as JsonResult;
return response?.Value != null ? JsonConvert.SerializeObject(response.Value) : "";
}
private EmResponseStaus GetResponseStatus(string pathSmall, string outParam)
{
if (string.IsNullOrWhiteSpace(outParam))
{
return EmResponseStaus.ReqNull;
}
try
{
//如果是TesTaskNotice接口,则使用TesResponse因为它的返回值和ApiResponse不一样
if (pathSmall.Contains("TesTaskNotice"))
{
var apiResponse = outParam.ToObject<TesResponse>();
return apiResponse?.ReturnCode == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
}
else
{
var apiResponse = outParam.ToObject<ApiResponse>();
return apiResponse?.Code == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
}
}
catch (JsonException)
{
logger.LogError($"在记录http日志时,序列化返参失败,path:{pathSmall} | outParam:{outParam}");
return EmResponseStaus.Unknown;
}
catch (Exception ex)
{
logger.LogError($"在记录http日志时,序列化返参失败,走到了Catch中,msg:{ex.Message},path:{pathSmall} | outParam:{outParam}");
return EmResponseStaus.Unknown;
}
}
/// <summary>
/// 执行Action之前
/// </summary>
/// <param name="context"></param>
public void OnActionExecuting(ActionExecutingContext context)
{
SkipActionFilterAttribute? skipActionFilterAttribute = context.ActionDescriptor.EndpointMetadata.OfType<SkipActionFilterAttribute>().FirstOrDefault();
// 判断是否存在 SkipActionFilterAttribute 特性
bool hasSkipActionFilter = skipActionFilterAttribute != null;
var (method, path, clientIp) = GetRequestInfo(context.HttpContext);
if (method == "GET" || path.Contains("Test") || hasSkipActionFilter) return; // 忽略GET请求和测试控制器
string inParam = GetInParam(context);
//当前请求的唯一ID
string requestId = context.HttpContext.TraceIdentifier;
HttpApiLogEntity httpApiLogEntity = new HttpApiLogEntity()
{
Method = Tool.ToMethodEnum(method),
Url = path,
IPAddress = clientIp,
InParam = inParam,
OutParam = "",
ResponseStaus = EmResponseStaus.Unknown,
IsIncomingRequest = true,
SystemType = EmSystemType.WCS,
RequestId = requestId,
CreateTime = DateTime.Now,
};
dbContext.HttpApiLogEntity.Insert(httpApiLogEntity);
if (!context.ModelState.IsValid)//如果在进入Action之前 就已经判断到入参有误 则直接返回不进入Action
{
List<string>? errors = context.ModelState.SelectMany(x => x.Value.Errors)
.Select(x => x.ErrorMessage)
.ToList();
ApiResponse? outParam = new ApiResponse
{
Code = EmApiResCode.ReqError, //入参有误 返回2
Msg = string.Join(',', errors),
Data = false
};
logger.LogInformation(
$"Method: {method}, Path: {path}, IP: {clientIp}, InParam: {inParam}, OutParam: {outParam.ToJson()}");
httpApiLogEntity.OutParam = outParam.ToJson();
httpApiLogEntity.ResponseStaus = outParam.Code == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
httpApiLogEntity.EndTime = DateTime.Now;
dbContext.HttpApiLogEntity.Update(httpApiLogEntity);
context.Result = new JsonResult(outParam);
}
}
/// <summary>
/// 执行Action之后
/// </summary>
/// <param name="context"></param>
public void OnActionExecuted(ActionExecutedContext context)
{
SkipActionFilterAttribute? skipActionFilterAttribute = context.ActionDescriptor.EndpointMetadata.OfType<SkipActionFilterAttribute>()
.FirstOrDefault();
// 判断是否存在 SkipActionFilterAttribute 特性
bool hasSkipActionFilter = skipActionFilterAttribute != null;
var (method, path, clientIp) = GetRequestInfo(context.HttpContext);
string pathSmall = context.HttpContext.Request.Path.ToString();
if (method == "GET" || pathSmall.Contains("Test") || hasSkipActionFilter) return; // 忽略GET请求和测试控制器
string requestId = context.HttpContext.TraceIdentifier;
HttpApiLogEntity? httpApiLogEntity = dbContext.HttpApiLogEntity.GetFirst(x => x.RequestId == requestId);
if (httpApiLogEntity != null)
{
string outParam = GetOutParam(context);
httpApiLogEntity.OutParam = outParam;
httpApiLogEntity.ResponseStaus = GetResponseStatus(pathSmall, outParam);
httpApiLogEntity.EndTime = DateTime.Now;
dbContext.HttpApiLogEntity.Update(httpApiLogEntity);
}
}
}
该类实现了IActionFilter
接口,该接口定义了OnActionExecuting
和OnActionExecuted
方法,分别在动作方法执行前和执行后调用。同时实现了IOrderedFilter
接口,用于指定过滤器的执行顺序。
2. 构造函数
/// <summary>
/// 构造
/// </summary>
/// <param name="logger"></param>
public GlobalActionFilter(SugarDbContext dbContext, ILogger<GlobalActionFilter> logger)
{
this.dbContext = dbContext;
this.logger = logger;
}
private readonly SugarDbContext dbContext;
private readonly ILogger<GlobalActionFilter> logger;
构造函数接收数据库上下文对象SugarDbContext
和日志记录器ILogger<GlobalActionFilter>
,用于后续的数据库操作和日志记录。
3. 执行顺序
/// <summary>
/// 过滤器执行顺序
/// </summary>
public int Order => 2; // 设置执行顺序
通过实现IOrderedFilter
接口的Order
属性,指定该过滤器的执行顺序为2。数值越小,过滤器越先执行。
4. 获取请求信息
private (string Method, string Path, string ClientIp) GetRequestInfo(HttpContext context)
{
string method = context.Request.Method;
string serverIp = context.Connection.LocalIpAddress.GetFormattedIpAddress();
string serverBaseUrl = $"http://{serverIp}:{context.Connection.LocalPort}";
string pathSmall = context.Request.Path.ToString();
string path = serverBaseUrl + pathSmall;
string clientIp = context.Connection.RemoteIpAddress.GetFormattedIpAddress();
return (method, path, clientIp);
}
该方法从HttpContext
中提取请求方法、完整请求路径和客户端IP地址,并以元组形式返回。
5. 获取输入参数
private string GetInParam(ActionExecutingContext context)
{
return context.ActionArguments.Values.FirstOrDefault()?.ToJson() ?? "Null";
}
从ActionExecutingContext
的ActionArguments
中获取动作方法的输入参数,并序列化为JSON字符串。如果没有参数,则返回”Null”。
6. 获取输出参数
private string GetOutParam(ActionExecutedContext context)
{
JsonResult? response = context.Result as JsonResult;
return response?.Value != null ? JsonConvert.SerializeObject(response.Value) : "";
}
从ActionExecutedContext
的Result
中获取动作方法的返回结果,如果是JsonResult
类型,则将其值序列化为JSON字符串返回。
7. 获取响应状态
private EmResponseStaus GetResponseStatus(string pathSmall, string outParam)
{
if (string.IsNullOrWhiteSpace(outParam))
{
return EmResponseStaus.ReqNull;
}
try
{
//如果是TesTaskNotice接口,则使用TesResponse因为它的返回值和ApiResponse不一样
if (pathSmall.Contains("TesTaskNotice"))
{
var apiResponse = outParam.ToObject<TesResponse>();
return apiResponse?.ReturnCode == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
}
else
{
var apiResponse = outParam.ToObject<ApiResponse>();
return apiResponse?.Code == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
}
}
catch (JsonException)
{
logger.LogError($"在记录http日志时,序列化返参失败,path:{pathSmall} | outParam:{outParam}");
return EmResponseStaus.Unknown;
}
catch (Exception ex)
{
logger.LogError($"在记录http日志时,序列化返参失败,走到了Catch中,msg:{ex.Message},path:{pathSmall} | outParam:{outParam}");
return EmResponseStaus.Unknown;
}
}
根据响应结果和请求路径判断响应状态。如果是特定接口TesTaskNotice
,则根据TesResponse
的ReturnCode
判断;否则根据ApiResponse
的Code
判断。如果序列化失败,则记录错误日志并返回未知状态。
8. 动作执行前
/// <summary>
/// 执行Action之前
/// </summary>
/// <param name="context"></param>
public void OnActionExecuting(ActionExecutingContext context)
{
SkipActionFilterAttribute? skipActionFilterAttribute = context.ActionDescriptor.EndpointMetadata.OfType<SkipActionFilterAttribute>().FirstOrDefault();
// 判断是否存在 SkipActionFilterAttribute 特性
bool hasSkipActionFilter = skipActionFilterAttribute != null;
var (method, path, clientIp) = GetRequestInfo(context.HttpContext);
if (method == "GET" || path.Contains("Test") || hasSkipActionFilter) return; // 忽略GET请求和测试控制器
string inParam = GetInParam(context);
//当前请求的唯一ID
string requestId = context.HttpContext.TraceIdentifier;
HttpApiLogEntity httpApiLogEntity = new HttpApiLogEntity()
{
Method = Tool.ToMethodEnum(method),
Url = path,
IPAddress = clientIp,
InParam = inParam,
OutParam = "",
ResponseStaus = EmResponseStaus.Unknown,
IsIncomingRequest = true,
SystemType = EmSystemType.WCS,
RequestId = requestId,
CreateTime = DateTime.Now,
};
dbContext.HttpApiLogEntity.Insert(httpApiLogEntity);
if (!context.ModelState.IsValid)//如果在进入Action之前 就已经判断到入参有误 则直接返回不进入Action
{
List<string>? errors = context.ModelState.SelectMany(x => x.Value.Errors)
.Select(x => x.ErrorMessage)
.ToList();
ApiResponse? outParam = new ApiResponse
{
Code = EmApiResCode.ReqError, //入参有误 返回2
Msg = string.Join(',', errors),
Data = false
};
logger.LogInformation(
$"Method: {method}, Path: {path}, IP: {clientIp}, InParam: {inParam}, OutParam: {outParam.ToJson()}");
httpApiLogEntity.OutParam = outParam.ToJson();
httpApiLogEntity.ResponseStaus = outParam.Code == 0 ? EmResponseStaus.Success : EmResponseStaus.Fail;
httpApiLogEntity.EndTime = DateTime.Now;
dbContext.HttpApiLogEntity.Update(httpApiLogEntity);
context.Result = new JsonResult(outParam);
}
}
在动作方法执行前,首先判断是否应跳过该过滤器(例如GET请求、测试控制器或标记了SkipActionFilterAttribute
特性的方法)。然后获取请求信息和输入参数,创建HttpApiLogEntity
对象并插入数据库。如果模型状态无效(入参有误),则构造错误响应,更新日志记录并返回错误响应。
9. 动作执行后
/// <summary>
/// 执行Action之后
/// </summary>
/// <param name="context"></param>
public void OnActionExecuted(ActionExecutedContext context)
{
SkipActionFilterAttribute? skipActionFilterAttribute = context.ActionDescriptor.EndpointMetadata.OfType<SkipActionFilterAttribute>()
.FirstOrDefault();
// 判断是否存在 SkipActionFilterAttribute 特性
bool hasSkipActionFilter = skipActionFilterAttribute != null;
var (method, path, clientIp) = GetRequestInfo(context.HttpContext);
string pathSmall = context.HttpContext.Request.Path.ToString();
if (method == "GET" || pathSmall.Contains("Test") || hasSkipActionFilter) return; // 忽略GET请求和测试控制器
string requestId = context.HttpContext.TraceIdentifier;
HttpApiLogEntity? httpApiLogEntity = dbContext.HttpApiLogEntity.GetFirst(x => x.RequestId == requestId);
if (httpApiLogEntity != null)
{
string outParam = GetOutParam(context);
httpApiLogEntity.OutParam = outParam;
httpApiLogEntity.ResponseStaus = GetResponseStatus(pathSmall, outParam);
httpApiLogEntity.EndTime = DateTime.Now;
dbContext.HttpApiLogEntity.Update(httpApiLogEntity);
}
}
在动作方法执行后,同样判断是否应跳过该过滤器。然后根据请求ID从数据库中获取之前插入的日志记录,获取输出参数和响应状态,更新日志记录的输出参数、响应状态和结束时间。
三、使用过滤器
1. 注册过滤器
在Startup.cs
文件的ConfigureServices
方法中注册过滤器:
public void ConfigureServices(IServiceCollection services)
{
// 其他服务注册...
services.AddControllers(options =>
{
options.Filters.Add<GlobalActionFilter>();
});
}
这样,该过滤器将应用于所有控制器的动作方法。如果只希望应用于特定控制器或动作方法,可以在控制器类或动作方法上添加[TypeFilter(typeof(GlobalActionFilter))]
特性。
2. 示例
假设我们有一个简单的控制器:
using Microsoft.AspNetCore.Mvc;
namespace Project.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class WeatherForecastController : ControllerBase
{
[HttpPost]
public IActionResult Post([FromBody] WeatherForecast forecast)
{
// 处理逻辑...
return Ok(new { Message = "Success" });
}
}
}
当发送POST请求到该控制器的Post
方法时,GlobalActionFilter
将记录请求和响应信息到数据库中。
四、总结
通过自定义动作过滤器,我们可以方便地在ASP.NET Core应用中记录Http API日志。这不仅有助于系统的调试和维护,还能提供有价值的运行时信息。在实际应用中,可以根据具体需求对过滤器进行扩展和优化,例如添加更多的日志字段、支持不同的日志存储方式等。
希望本文能帮助你理解和使用ASP.NET Core中的请求过滤器来记录API日志。如果有任何问题或建议,欢迎留言讨论。
来源链接:https://www.cnblogs.com/cyfj/p/18849970
如有侵犯您的版权,请及时联系3500663466#qq.com(#换@),我们将第一时间删除本站数据。
暂无评论内容