ASP.NET Core 6 使用 Middleware 攔截 Request 和 Response

在 ASP.NET Core 6 中該如何紀錄每一個 API 的請求(Request)和回應(Response)資訊呢?很簡單,就是使用中介軟體(Middleware),很容易的就能夠攔截到 API 的資訊,再來就只是如何儲存了。

建立攔截的中介軟體
    
public class ApiLoggingMiddleware
{
    private readonly ILogger<ApiLoggingMiddleware> _logger;

    private readonly RequestDelegate _next;

    public ApiLoggingMiddleware(ILogger<ApiLoggingMiddleware> logger, RequestDelegate next)
    {
        _logger = logger;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 攔截請求
        var request = await FormatRequest(context.Request);

        // 紀錄回應前的狀態
        var originalBodyStream = context.Response.Body;

        using var responseBody = new MemoryStream();
        context.Response.Body = responseBody;

        await _next(context);

        // 攔截回應
        var response = await FormatResponse(context.Response);

        // 紀錄資訊
        _logger.LogInformation("Request: \n{Request}", request);
        _logger.LogInformation("Response: \n{Response}", response);


        // 將原始回應內容寫回
        await responseBody.CopyToAsync(originalBodyStream);
    }

    /// <summary>
    /// 格式化請求
    /// </summary>
    private async Task<string> FormatRequest(HttpRequest request)
    {
        request.EnableBuffering();
        var headers = FormatHeaders(request.Headers);
        var body = await new StreamReader(request.Body, Encoding.UTF8).ReadToEndAsync();
        request.Body.Position = 0;
        var ip = request.HttpContext.Connection.RemoteIpAddress?.ToString();

        return $"Method: {request.Method}\n" +
               $"URL: {request.Scheme}://{request.Host}{request.Path}{request.QueryString}\n" +
               $"Headers: \n{headers}" +
               $"Body: {body}\n" +
               $"IP: {ip}";
    }

    /// <summary>
    /// 格式化回應
    /// </summary>
    private async Task<string> FormatResponse(HttpResponse response)
    {
        response.Body.Seek(0, SeekOrigin.Begin);
        var headers = FormatHeaders(response.Headers);
        var body = await new StreamReader(response.Body, Encoding.UTF8).ReadToEndAsync();
        response.Body.Seek(0, SeekOrigin.Begin);

        return $"Status code: {response.StatusCode}\n" +
               $"Headers: \n{headers}" +
               $"Body: {body}";
    }

    /// <summary>
    /// 格式化標頭
    /// </summary>
    private string FormatHeaders(IHeaderDictionary headers)
    {
        var formattedHeaders = new StringBuilder();
        foreach (var (key, value) in headers)
        {
            formattedHeaders.AppendLine($"\t{key}: {string.Join(",", value)}");
        }

        return formattedHeaders.ToString();
    }
}
    

使用中介軟體
    
var app = builder.Build();

app.UseMiddleware<ApiLoggingMiddleware>();
    

測試用的範例 API:
    
[ApiController]
public class UserController : ControllerBase
{
    [HttpGet("/api/users")]
    public ActionResult GetUsers([FromQuery] int userId)
    {
        return Ok(
            new List<object>
            {
                new
                {
                    Id = 1,
                    Name = "User1"
                }
            }
        );
    }
}
    

範例輸出:
    
info: Request:
Method: GET
URL: https://localhost:7170/api/users?userId=123
Headers:
        Accept: */*
        Connection: keep-alive
        Host: localhost:7170
        User-Agent: PostmanRuntime/7.32.3
        Accept-Encoding: gzip, deflate, br
        Postman-Token: 4cbdfd7e-3a01-42b7-ae12-3e23181628fe
Body:
IP: 127.0.0.1

info: Response:
Status code: 200
Headers:
        Content-Type: application/json; charset=utf-8
Body: [{"id":1,"name":"User1"}]
    

目前是通通寫在 log 中,這樣所有 API 請求回應和一般的 log 都混在一起,要除錯有夠麻煩,該怎麼辦?
那可以參考看看下方的連結,是昨天的文章,介紹使用 Serilog 套件輸出 log 時該如何將內容拆分為多個檔案。

延伸閱讀:C# ASP.NET Core 6 依照功能拆分 Serilog 套件輸出的 log 檔案

留言