前言:
我们日常工作中经常需要日志记录,常见的方式比如基于框架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<IServerAddressesFeature>();
if (serverAddressFeature != null)
{
//实际上是UseMiddleware泛型注入HttpsRedirectionMiddleware
app.UseMiddleware<HttpsRedirectionMiddleware>(serverAddressFeature);
}
else
{
app.UseMiddleware<HttpsRedirectionMiddleware>();
}
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<string> _ignoreActions = new List<string> { "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<ITokenService>();
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<ITApilogServices>();
try
{
/*这里持久化执行流程,逻辑自定,因为这里是记录到数据库(mongo)的,所以字段长度在设计的时候要足够,同时因为这个表查询频率不高,可以不建任何索引(这个表空间的增长速度会非常快,所以个人认为没必要增加开销)*/
await _tApilogServices.InsertAsync(apilog);
}
catch
{
// ignored
}
}
else
{
//传递个下一个中间件
await _next(context);
}
}
//解析返回值
private async Task<string> 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;
}
}
测试:
以上就是代码的全部实现了,是一个比较简单的实现,在生产环境还是建议使用框架进行实现。