ASP.NET Core自定义响应内容
Posted dotNET跨平台
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ASP.NET Core自定义响应内容相关的知识,希望对你有一定的参考价值。
问题
在业务开发中,对Web API的返回格式有一定要求,需要是定制化的Json结构,用于前端统一处理:
{
Status : 0,
Message: "",
Info : xxx
}
Status
表示响应的状态码,0为成功;Message
表示错误消息,Status不为0时返回;Info
表示API返回的实际数据,Json格式;
简单实现
当然,你可以定义一个数据结构作为每个API的返回值:
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下已经无法改写,无任何数据返回。示例代码如下:
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的源代码中找到了ResponseCompressionMiddleware(https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/ResponseCompression/src/ResponseCompressionMiddleware.cs)。
它是用来处理响应压缩的中间件,也就是说对响应做了处理,看看它的实现方式:
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
进行了替换:
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
:
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();
}
}
关键代码就是下面这段,我们取出原始响应内容,格式化后再写入:
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:
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);
}
}
}
运行效果也能满足我们的要求:
结论
在本文中,我们利用了ASP.NET Core的功能接口实现了自定义格式响应。
你也可以尝试使用功能接口实现一些有用的功能。
如果你觉得这篇文章对你有所启发,请关注我的个人公众号”My IO“,记住我!
以上是关于ASP.NET Core自定义响应内容的主要内容,如果未能解决你的问题,请参考以下文章