C# DotNet Core 中间件封装响应

11

我有一个简单的控制器动作,看起来像这样:

    public Task<IEnumerable<Data>> GetData()
    {
        IEnumerable<Data> data = new List<Data>();
        return data;
    }

我希望能够在中间件内检查返回值,因此JSON会类似于:

{
  "data": [
  ],
  "apiVersion": "1.2",
  "otherInfoHere": "here"
}

所以我的有效载荷始终在data中。我知道我可以在控制器级别上做到这一点,但我不想在每个操作上都这样做。我宁愿在中间件中为所有操作仅执行一次。

以下是我的中间件示例:

public class NormalResponseWrapper
{
    private readonly RequestDelegate next;

    public NormalResponseWrapper(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {                
        var obj = context;
        // DO something to get return value from obj
        // Create payload and set data to return value

        await context.Response.WriteAsync(/*RETURN NEW PAYLOAD HERE*/);
    }

有任何想法吗?

现在已经得到这个值,但是太晚返回它了。

        try
        {
            using (var memStream = new MemoryStream())
            {
                context.Response.Body = memStream;
                await next(context);
                memStream.Position = 0;
                object responseBody = new StreamReader(memStream).ReadToEnd();
                memStream.Position = 0;
                await memStream.CopyToAsync(originalBody);
                // By now it is to late, above line sets the value that is going to be returned
                await context.Response.WriteAsync(new BaseResponse() { data = responseBody }.toJson());
            }

        }
        finally
        {
            context.Response.Body = originalBody;
        }

看一下这个,看看它是否适用 https://dev59.com/fFcQ5IYBdhLWcg3wD_ho - Nkosi
这样可以获取数据,但当我操作它并尝试返回时,已经太晚了。 - Lemex
这个会有内存问题的风险吗? - Lemex
与大负载相关的常见问题没有什么不同。请注意,这种方法不适用于流式传输。 - Nkosi
这是我目前找到的最佳实现链接:https://vmsdurano.com/autowrapper-prettify-your-asp-net-core-apis-with-meaningful-responses/ - DSR
2个回答

8
在 .NET Core 3.1 或 .NET 5 中
  1. Create your response envelope object. Example:

    internal class ResponseEnvelope<T>
    {
      public T Data { set; get; }
      public string ApiVersion { set; get; }
      public string OtherInfoHere { set; get; }
    }
    
  2. Derive a class from ObjectResultExecutor

    internal class ResponseEnvelopeResultExecutor : ObjectResultExecutor
    {
     public ResponseEnvelopeResultExecutor(OutputFormatterSelector formatterSelector, IHttpResponseStreamWriterFactory writerFactory, ILoggerFactory loggerFactory, IOptions<MvcOptions> mvcOptions) : base(formatterSelector, writerFactory, loggerFactory, mvcOptions)
     {
     }
    
     public override Task ExecuteAsync(ActionContext context, ObjectResult result)
     {
         var response = new ResponseEnvelope<object>();
         response.Data = result.Value;
         response.ApiVersion = "v1";
         response.OtherInfoHere = "OtherInfo";
    
         TypeCode typeCode = Type.GetTypeCode(result.Value.GetType());
         if (typeCode == TypeCode.Object)
              result.Value = response;
    
         return base.ExecuteAsync(context, result);
      }
    }
    
  3. Inject into the DI like

    public void ConfigureServices(IServiceCollection services)
     {
         services.AddSingleton<IActionResultExecutor<ObjectResult>, ResponseEnvelopeResultExecutor>();
    

响应应该有一个封装。但是对于原始类型无效。


除了更简单之外,它也不会遇到像@Konrad指出的假定媒体类型等问题。 - Chris Martinez
你能解释一下你的代码吗?为什么要这样写?TypeCode typeCode = Type.GetTypeCode(result.Value.GetType()); if (typeCode == TypeCode.Object) - Rasik
1
为了避免使用原始类型。https://learn.microsoft.com/en-us/dotnet/api/system.typecode?view=net-5.0 - Xavier John
@XavierJohn;这是一个聪明而快速的解决方案。你真是个天才! - XAMT

6

阅读评论以了解您可以采取哪些方法来包装响应。

public async Task Invoke(HttpContext context) {
    //Hold on to original body for downstream calls
    Stream originalBody = context.Response.Body;
    try {
        string responseBody = null;
        using (var memStream = new MemoryStream()) {
            //Replace stream for upstream calls.
            context.Response.Body = memStream;
            //continue up the pipeline
            await next(context);
            //back from upstream call.
            //memory stream now hold the response data
            //reset position to read data stored in response stream
            memStream.Position = 0;
            responseBody = new StreamReader(memStream).ReadToEnd();
        }//dispose of previous memory stream.
        //lets convert responseBody to something we can use
        var data = JsonConvert.DeserializeObject(responseBody);
        //create your wrapper response and convert to JSON
        var json = new BaseResponse() { 
            data = data, 
            apiVersion = "1.2",
            otherInfoHere = "here"
        }.toJson();
        //convert json to a stream
        var buffer = Encoding.UTF8.GetBytes(json);
        using(var output = new MemoryStream(buffer)) {
            await output.CopyToAsync(originalBody);
        }//dispose of output stream
    } finally {
        //and finally, reset the stream for downstream calls
        context.Response.Body = originalBody;
    }
} 

我认为这种方法不好,因为它是在HTTP层面上处理的,如果你决定使用其他序列化格式,比如XML或者protobuf,它就会出问题。我会在其他地方做这样的事情。 - Konrad
每次都必须反序列化响应。这种包装应该在序列化之前发生。 - Konrad

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接