前言:
我们日常工作中经常需要日志记录,常见的方式比如基于框架Log4net,NLog,Serilog,或者基于过滤器方式实现基于控制器/方法级别的记录,然后今天我们基于请求管道特性使用app.UseMiddleware方式实现全量请求记录。
什么是请求管道?
在 ASP.NET Core 中,ConfigureServices 和 Configure 是 Startup 类中的两个重要方法,用于配置应用程序的服务和请求处理管道。
- ConfigureServices 方法用于配置应用程序的服务容器,注册应用程序所需的依赖项和服务。
- Configure 方法用于配置应用程序的请求处理管道,定义中间件和处理程序的顺序和逻辑。
请求处理管道定义了请求在应用程序中的处理流程,从请求进入应用程序开始,到最终生成响应返回给客户端。每个中间件都负责处理请求的某个方面或执行特定的功能。中间件可以执行各种任务,例如身份验证、授权、日志记录、异常处理、路由、静态文件服务等。
在请求处理管道中,每个中间件的顺序很重要,因为它们按照添加到管道的顺序依次执行。每个中间件的输出作为下一个中间件的输入,并且可以在中间件之间传递上下文对象(如 HttpContext)来共享数据和状态。
简单的源码探析:
1. 我们以 app.UseHttpsRedirection() 为例,进入UseHttpsRedirection方法
//可以看到UseHttpsRedirection是对app.UseMiddleware能力的一个封装 public static IApplicationBuilder UseHttpsRedirection(this IApplicationBuilder app) { ArgumentNullException.ThrowIfNull(app); var serverAddressFeature = app.ServerFeatures.Get
(); if (serverAddressFeature != null) { //实际上是UseMiddleware泛型注入HttpsRedirectionMiddleware app.UseMiddleware (serverAddressFeature); } else { app.UseMiddleware (); } return app; } 2. 我们看一下UseMiddleware方法的实现:
public static IApplicationBuilder UseMiddleware( this IApplicationBuilder app, [DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware, params object?[] args) { if (typeof(IMiddleware).IsAssignableFrom(middleware)) { // IMiddleware doesn't support passing args directly since it's // activated from the container if (args.Length > 0) { throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware))); } var interfaceBinder = new InterfaceMiddlewareBinder(middleware); //实际上是借用CreateMiddleware方法进行InvokeAsync方法调用 return app.Use(interfaceBinder.CreateMiddleware); } ......省略后续代码 }
3.看一下CreateMiddleware方法,发现其实际是从IMiddlewareFactory拿到注入的中间实例,然后调用其内部的InvokeAsync方法。
public RequestDelegate CreateMiddleware(RequestDelegate next) { return async context => { var middlewareFactory = (IMiddlewareFactory?)context.RequestServices.GetService(typeof(IMiddlewareFactory)); if (middlewareFactory == null) { // No middleware factory throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory))); } //通过IMiddlewareFactory获取中间件的实例 var middleware = middlewareFactory.Create(_middlewareType); if (middleware == null) { // The factory returned null, it's a broken implementation throw new InvalidOperationException(Resources.FormatException_UseMiddlewareUnableToCreateMiddleware(middlewareFactory.GetType(), _middlewareType)); } try { //这里实际去调用实例的方法 await middleware.InvokeAsync(context, next); } finally { middlewareFactory.Release(middleware); } }; }
简单总结一下app.UseMiddleware的实现:
1.通过泛型注入类型;
2.通过IMiddlewareFactory 的create方法,实际上是通过serviceProvider拿到实例。
public IMiddleware? Create(Type middlewareType) { return _serviceProvider.GetRequiredService(middlewareType) as IMiddleware; }
3.调用该实例的InvokeAsync方法
代码实现:
1.定义实体记录输入输出值,方便我们进行持久化
public class TApilog { //Ip地址 public string? Ip { get; set; } //请求方法 public string Action { get; set; } //请求打入时间 public string Intime { get; set; } //请求参数 public string Input { get; set; } //返回值 public string Output { get; set; } //请求结束时间 public string Outtime { get; set; } }
2.定义我们自己的中间件,我们可以实现接口IMiddleware,也可以不实现,只要定义InvokeAsync
方法供调用就可。
①先看一下IMiddleware的结构:
public interface IMiddleware { Task InvokeAsync(HttpContext context, RequestDelegate next); }
②具体实现代码
public class WebApiLog { private readonly RequestDelegate _next; private readonly IServiceScopeFactory _serviceScopeFactory; //这里可以设置一些我们不想记录的路径的日志,通常配置在配置文件,这里简化 private readonly List
_ignoreActions = new List { "Index1", "Default/Index2" }; public WebApiLog(RequestDelegate next, IServiceScopeFactory serviceScopeFactory) { _next = next; _serviceScopeFactory = serviceScopeFactory; } public async Task InvokeAsync(HttpContext context) { if (!_ignoreActions.Exists(s => context.Request.Path.ToString().Contains(s))) { //首先记录一些基本的参数,IP,Action,Time等 TApilog apilog = new TApilog(); apilog.Ip = Convert.ToString(context.Connection.RemoteIpAddress); apilog.Action = context.Request.Path; apilog.Intime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); //这里可以保存userToken using var scope = _serviceScopeFactory.CreateScope(); /*string token = context.Request.Headers["token"]; if (!string.IsNullOrEmpty(token)) { var tokenService = scope.ServiceProvider.GetRequiredService (); Apilog.Useraccount = tokenService.ParseToken(context)?.UserAccount; }*/ //传入参数解析拼接 StringBuilder inarg = new StringBuilder(); if (context.Request.HasFormContentType) { foreach (var item in context.Request.Form) { inarg.AppendLine(item.Key + ":" + item.Value); } } else if (context.Request.Query.Count > 0) { foreach (var item in context.Request.Query) { inarg.AppendLine(item.Key + ":" + item.Value); } } else { context.Request.EnableBuffering(); StreamReader streamReader = new StreamReader(context.Request.Body); inarg.AppendLine(await streamReader.ReadToEndAsync()); context.Request.Body.Seek(0, SeekOrigin.Begin); } apilog.Input = inarg.ToString(); //返回值解析 var originalBodyStream = context.Response.Body; using (var responseBody = new MemoryStream()) { context.Response.Body = responseBody; await _next(context); apilog.Output = await GetResponse(context.Response); await responseBody.CopyToAsync(originalBodyStream); } apilog.Outtime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); var _tApilogServices = scope.ServiceProvider.GetRequiredService (); try { /*这里持久化执行流程,逻辑自定,因为这里是记录到数据库(mongo)的,所以字段长度在设计的时候要足够,同时因为这个表查询频率不高,可以不建任何索引(这个表空间的增长速度会非常快,所以个人认为没必要增加开销)*/ await _tApilogServices.InsertAsync(apilog); } catch { // ignored } } else { //传递个下一个中间件 await _next(context); } } //解析返回值 private async Task GetResponse(HttpResponse response) { response.Body.Seek(0, SeekOrigin.Begin); var text = await new StreamReader(response.Body).ReadToEndAsync(); response.Body.Seek(0, SeekOrigin.Begin); return text; } } 测试:
以上就是代码的全部实现了,是一个比较简单的实现,在生产环境还是建议使用框架进行实现。
还没有评论,来说两句吧...