ASP.NET Core 自定义响应内容

问题

在业务开发中,对 Web API 的返回格式有一定要求,需要是定制化的 Json 结构,用于前端统一处理:

1
2
3
4
5
{
    Status : 0,
    Message: "",
    Info : xxx
}
  • Status 表示响应的状态码,0 为成功;
  • Message 表示错误消息,Status 不为 0 时返回;
  • Info 表示 API 返回的实际数据,Json 格式;

简单实现

当然,你可以定义一个数据结构作为每个 API 的返回值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ResponseData<T>
{
    public int Status { get; set; } = 0;
    public string Message { get; set; }
    public T Info { get; set; }

    public ResponseData(T obj)
    {
        Info = obj;
    }
}

[HttpGet]
public ResponseData<IEnumerable<WeatherForecast>> Get()
{
    var rng = new Random();
    var data = Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
    .ToArray();

    return new ResponseData<IEnumerable<WeatherForecast>>(data);
}

但是如果这样实现,每一个 API 方法都必须修改,实例化一个 ResponseData 对象返回。如果以后业务修改,要移除这个自定义结构又是件麻烦事。

有没有一劳永逸、并且更加优雅的实现方式呢?

自定义响应内容

既然这个 Json 结构是在原有的返回数据外围再包了一层,那么我们直接获取 Web API 的原始 Response.Body,然后格式化成新的 Json 在赋值给 Response.Body 不就可以了!

但是,实际验证时发现在 .NET 5 下已经无法改写,无任何数据返回。示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
app.Use(async (context, next) =>
{
    var newContent = string.Empty;

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

        await next();

        context.Response.Body = new MemoryStream();

        newBody.Seek(0, SeekOrigin.Begin);

        newContent = new StreamReader(newBody).ReadToEnd();

        newContent += ", World!";

        await context.Response.WriteAsync(newContent);
    }
});

IHttpResponseBodyFeature

aspnetcore 的源代码中找到了 ResponseCompressionMiddlewarehttps://github.com/dotnet/aspnetcore/blob/main/src/Middleware/ResponseCompression/src/ResponseCompressionMiddleware.cs )。

它是用来处理响应压缩的中间件,也就是说对响应做了处理,看看它的实现方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public async Task Invoke(HttpContext context)
{
    if (!_provider.CheckRequestAcceptsCompression(context))
    {
        await _next(context);
        return;
    }

    var originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>();
    var originalCompressionFeature = context.Features.Get<IHttpsCompressionFeature>();

    Debug.Assert(originalBodyFeature != null);

    var compressionBody = new ResponseCompressionBody(context, _provider, originalBodyFeature);
    context.Features.Set<IHttpResponseBodyFeature>(compressionBody);
    context.Features.Set<IHttpsCompressionFeature>(compressionBody);

    try
    {
        await _next(context);
        await compressionBody.FinishCompressionAsync();
    }
    finally
    {
        context.Features.Set(originalBodyFeature);
        context.Features.Set(originalCompressionFeature);
    }
}

它将 IHttpResponseBodyFeature 进行了替换:

1
context.Features.Set<IHttpResponseBodyFeature>(compressionBody);

IHttpResponseBodyFeature 到底是个什么玩意?

“ASP.NET Core 中的请求功能”( https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/request-features?view=aspnetcore-5.0 )作出了相应的解释:

ASP.NET Core 在 Microsoft.AspNetCore.Http.Features 中定义了许多常见的 HTTP 功能接口,各种服务器和中间件共享这些接口来标识其支持的功能。服务器和中间件还可以提供自己的具有附加功能的接口。

ResponseCustomBody

那我们就依葫芦画瓢,实现我们的 ResponseCustomBody:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class ResponseCustomBody : Stream, IHttpResponseBodyFeature
{
    private readonly HttpContext _context;
    private readonly IHttpResponseBodyFeature _innerBodyFeature;
    private readonly Stream _innerStream;

    public ResponseCustomBody(HttpContext context,
        IHttpResponseBodyFeature innerBodyFeature)
    {
        _context = context;
        _innerBodyFeature = innerBodyFeature;
        _innerStream = innerBodyFeature.Stream;
    } 
    public Stream Stream => this;

    public PipeWriter Writer => throw new NotImplementedException();

    public override bool CanRead => false;

    public override bool CanSeek => false;

    public override bool CanWrite => _innerStream.CanWrite;

    public override long Length => throw new NotImplementedException();

    public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }

    public async Task CompleteAsync()
    {
        await _innerBodyFeature.CompleteAsync();
    }

    public void DisableBuffering()
    {
        _innerBodyFeature.DisableBuffering();
    }

    public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default)
    {
        return _innerBodyFeature.SendFileAsync(path, offset, count, cancellationToken);
    }

    public Task StartAsync(CancellationToken cancellationToken = default)
    {
        return _innerBodyFeature.StartAsync(cancellationToken);
    }

    public override void Flush()
    {
        _innerStream.Flush();
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        throw new NotImplementedException();
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        throw new NotImplementedException();
    }

    public override void SetLength(long value)
    {
        throw new NotImplementedException();
    }
    
    public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {
        var json = System.Text.Encoding.UTF8.GetString(buffer).TrimEnd('\0');
        json = "{\"Status\":0, \"Info\":" + json + " }";
        buffer = System.Text.Encoding.UTF8.GetBytes(json);
        count = buffer.Length;
        await _innerStream.WriteAsync(buffer, offset, count, cancellationToken);
    }
    
    public override void Write(byte[] buffer, int offset, int count)
    {
        throw new NotImplementedException();
    }
}

关键代码就是下面这段,我们取出原始响应内容,格式化后再写入:

1
2
3
4
5
6
7
8
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
    var json = System.Text.Encoding.UTF8.GetString(buffer).TrimEnd('\0');
    json = "{\"Status\":0, \"Info\":" + json + " }";
    buffer = System.Text.Encoding.UTF8.GetBytes(json);
    count = buffer.Length;
    await _innerStream.WriteAsync(buffer, offset, count, cancellationToken);
}

最后,我们再定义一个中间件使用 ResponseCustomBody 替换 IHttpResponseBodyFeature

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class ResponseCustomMiddleware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>();

        var customBody = new ResponseCustomBody(context, originalBodyFeature);
        context.Features.Set<IHttpResponseBodyFeature>(customBody);

        try
        {
            await next(context);
        }
        finally
        {
            context.Features.Set(originalBodyFeature);
        }
    } 
}

运行效果也能满足我们的要求:

0%