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

文章介绍了如何在ASP.NETCore中使用`app.UseMiddleware`方法基于请求管道实现全量请求记录,包括中间件的概念、`UseMiddleware`的源码剖析以及一个简单的WebApiLog中间件示例,展示了如何捕获和记录请求参数和返回值。
摘要由CSDN通过智能技术生成

前言:

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

测试:

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值