利用 ASP.NET Core Middleware 統一處理回應訊息

當你的服務需要隱藏真實的訊息給調用端時,除了在每一個 Action 各自處理,Middleware 也是選擇之一,本想這應該是很簡單的操作,沒想到碰了一鼻子灰,以下是我複寫 HttpContext.Response.Body 的步驟,希望對正觀看的你有幫助。

開發環境

  • Windows 11
  • .NET 6
  • Rider 2022.2.1

Middleware 生命週期

Middleware 的執行順序是由 Startup 配置的順序決定,順序在上面的 Request 會先執行,順序在下面的 Response 會先執行

下圖出自 ASP.NET Core 中介軟體 | Microsoft Docs

要求處理模式說明要求抵達,經過三個中介軟體所處理,然後應用程式送出回應。每個中介軟體會執行其邏輯,並於 next() 陳述式中將要求遞交至下個中介軟體。在第三個中介軟體處理要求後,要求會反向傳回經前兩個中介軟體,以在其 next() 陳述式後,至作為回應離開應用程式傳到用戶端前的期間內,進行額外處理。

 

如何在 Middleware 複寫 HttpContext.Response.Body

在 Middleware 的 HttpContext.Response.Body (Stream) 是不可讀的,需要把它換成可讀的 MemoryStream,最後處理完畢之後,必須要再換回來

Step1. 換成可以讀取的 MemoryStream,如下圖:

 

Step2. HttpContext.Response.Body 換回原來的 Stream

這裡做了幾件事

  1. 讀取 Response.Body 的內容,並寫入到 log
  2. 替換原本的 Response.Body,把真實的內容換成不明確的 await context.Response.WriteAsync(fuzzyData);

 

首先,新增一個 .NET 6 的 Library 專案,並安裝以下套件

dotnet add package Microsoft.AspNetCore.Http.Abstractions --version 2.2.0
dotnet add package Microsoft.Extensions.Logging.Abstractions --version 6.0.1

 

OverrideResponseHandlerMiddleware 完整代碼如下:

public class OverrideResponseHandlerMiddleware
{
    private readonly RequestDelegate _next;

    public OverrideResponseHandlerMiddleware(RequestDelegate next)
    {
        this._next = next;
    }

    public async Task InvokeAsync(HttpContext context,
        ILogger<OverrideResponseHandlerMiddleware> logger,
        JsonSerializerOptions jsonSerializerOptions)
    {
        var originalResponseBodyStream = context.Response.Body;
        await using var newResponseBodyStream = new MemoryStream();
        context.Response.Body = newResponseBodyStream;

        await this._next(context);

        newResponseBodyStream.Seek(0, SeekOrigin.Begin);
        var statusCode = context.Response.StatusCode;
        var fuzzyBody = statusCode switch
        {
            401 => CreateFuzzyBody("NoAuthentication"),
            403 => CreateFuzzyBody("NoAuthorization"),
            _ => null
        };

        if (fuzzyBody != null)
        {
            var fuzzyData = JsonSerializer.Serialize(fuzzyBody, jsonSerializerOptions);
            logger.LogInformation("Fuzzy data:{FuzzyData}", fuzzyData);

            var realData = await new StreamReader(newResponseBodyStream).ReadToEndAsync();
            logger.LogInformation("Read data:{RealData}", realData);

            context.Response.Body = originalResponseBodyStream;
            await context.Response.WriteAsync(fuzzyData);
        }
        else
        {
            await newResponseBodyStream.CopyToAsync(originalResponseBodyStream);
            context.Response.Body = originalResponseBodyStream;
        }
    }

    private static object CreateFuzzyBody(string failureCode)
    {
        return new
        {
            ErrorCode = failureCode,
            ErrorMessage = "Please contact your administrator"
        };
    }
}

 

Middleware 的測試案例

新增一個 MsTest 測試專案並安裝以下套件

dotnet add package Microsoft.AspNetCore.TestHost --version 6.0.8

Middleware 的單元測試上一篇有比較完整的介紹,ASP.NET Core 6 Middleware 的單元測試 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

這裡我會使用 DefaultHttpContext 來完成這一次的演示

首先我用 DI Container,把我需要的物件都準好

private static IServiceProvider CreateServiceProvider()
{
    var services = new ServiceCollection();
    services.AddSingleton(p => CreateJsonSerializerOptions());
    services.AddSingleton(p => LoggerFactory.Create(builder => { builder.AddConsole(); }));
    services.AddSingleton(p => p.GetService<ILoggerFactory>().CreateLogger<OverrideResponseHandlerMiddleware>());
    return services.BuildServiceProvider();
}
private static JsonSerializerOptions CreateJsonSerializerOptions()
{
    return new()
    {
        Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin,
            UnicodeRanges.CjkUnifiedIdeographs),
        PropertyNameCaseInsensitive = true,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };
}

 

Middleware 需要下一關的 Next HttpContext,這裡回傳真實資料

private static Task CreateFakeNextContext(HttpContext context, object detailFailure, int statusCode)
{
    context.Response.StatusCode = statusCode;
    context.Response.WriteAsJsonAsync(detailFailure);
    return Task.CompletedTask;
}

到時候需要確保 OverrideResponseHandlerMiddleware 順序是在 Middleware Response 的最後一個

我預期 HttpStatus = 200 不會覆寫訊息 { Code = "9527" },測試案例如下

[TestMethod]
public async Task 不模糊訊息()
{
    var expected = @"{""code"":""9527""}";

    var serviceProvider = CreateServiceProvider();
    var jsonSerializerOptions = serviceProvider.GetService<JsonSerializerOptions>();
    var logger = serviceProvider.GetService<ILogger<OverrideResponseHandlerMiddleware>>();
    var target = new OverrideResponseHandlerMiddleware(nextContext =>
        CreateFakeNextContext(nextContext, new { Code = "9527" }, StatusCodes.Status200OK));

    var httpContext = new DefaultHttpContext
    {
        Response = { Body = new MemoryStream() }
    };

    await target.InvokeAsync(httpContext, logger, jsonSerializerOptions);
    var response = httpContext.Response;
    var stream = response.Body;
    if (stream.CanSeek)
    {
        stream.Seek(0, SeekOrigin.Begin);
    }

    var actual = await new StreamReader(stream).ReadToEndAsync();
    Assert.AreEqual(expected, actual);
}

 

當 HttpStatus = 403 覆寫訊息,不論本來回傳什麼,都會被覆寫訊息並寫入 log,測試案例如下

[TestMethod]
public async Task 模糊化未授權訊息()
{
    var expected = @"{""errorCode"":""NoAuthorization"",""errorMessage"":""Please contact your administrator""}";
    var httpContext = new DefaultHttpContext
    {
        Response = { Body = new MemoryStream() }
    };
    var serviceProvider = CreateServiceProvider();
    var jsonSerializerOptions = serviceProvider.GetService<JsonSerializerOptions>();
    var logger = serviceProvider.GetService<ILogger<OverrideResponseHandlerMiddleware>>();

    var target = new OverrideResponseHandlerMiddleware(nextContext =>
        CreateFakeNextContext(nextContext, new
        {
            ErrorCode = "NoAuthorization",
            ErrorMessage = "No permission"
        }, StatusCodes.Status403Forbidden));

    await target.InvokeAsync(httpContext, logger, jsonSerializerOptions);

    var response = httpContext.Response;
    var stream = response.Body;
    if (stream.CanSeek)
    {
        stream.Seek(0, SeekOrigin.Begin);
    }

    var actual = await new StreamReader(stream).ReadToEndAsync();
    Assert.AreEqual(expected, actual);
}

 

範例位置

sample.dotblog/AspNetCore/Lib.Middleware.OverrideResponse at master · yaochangyu/sample.dotblog (github.com)

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo