.Net 8 WebApi传入传出参数记录

.Net 8 WebApi传入传出参数记录

码农世界 2024-05-27 前端 69 次浏览 0个评论

前言:

我们日常工作中经常需要日志记录,常见的方式比如基于框架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;
        }
    }

    测试:

    以上就是代码的全部实现了,是一个比较简单的实现,在生产环境还是建议使用框架进行实现。 

转载请注明来自码农世界,本文标题:《.Net 8 WebApi传入传出参数记录》

百度分享代码,如果开启HTTPS请参考李洋个人博客
每一天,每一秒,你所做的决定都会改变你的人生!

发表评论

快捷回复:

评论列表 (暂无评论,69人围观)参与讨论

还没有评论,来说两句吧...

Top