11

I have a simple controller action which looks like:

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

I want to be able to inspect the return value from within the middleware so the JSON would look something like

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

So my payload always is within data. I know I can do this at a controller level but I don't wan to have to do it on every single action. I would rather do it in middleware once for all.

Here is an example of my middleware:

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*/);
    }

Any ideas?

Got the value now but it's to late to return it

        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;
        }
Lemex
  • 3,772
  • 14
  • 53
  • 87
  • Take a look at this and see if it applies https://stackoverflow.com/questions/43403941/how-to-read-asp-net-core-response-body – Nkosi Nov 08 '17 at 13:53
  • So that gets me the data but when I manipulate it and try return it it's to late – Lemex Nov 08 '17 at 14:39
  • Is there any risk of memory issues with this? – Lemex Nov 08 '17 at 16:21
  • Nothing out of the normal issues associated with large payloads. Do note that this approach will not work for streaming. – Nkosi Nov 08 '17 at 16:25
  • Here is the link for best implementation I found till now: https://vmsdurano.com/autowrapper-prettify-your-asp-net-core-apis-with-meaningful-responses/ – DSR Sep 16 '20 at 14:24

2 Answers2

8

In .NET Core 3.1 or .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>();
    

And the responses should have an envelope. This does not work with primitive types.

Xavier John
  • 8,474
  • 3
  • 37
  • 51
  • Aside from being simpler, it also doesn't suffer from the issues such as an assumed media type as pointed out by @Konrad above. – Chris Martinez Nov 02 '20 at 16:20
  • Can you explain a bit about your code? why you did? `TypeCode typeCode = Type.GetTypeCode(result.Value.GetType()); if (typeCode == TypeCode.Object)` – Rasik Jul 08 '21 at 06:48
  • 1
    To avoid primitive types. https://learn.microsoft.com/en-us/dotnet/api/system.typecode?view=net-5.0 – Xavier John Jul 29 '21 at 22:09
  • @XavierJohn; It is a smart and fast solution. You are a genius ! – XAMT Oct 11 '22 at 16:25
6

Review the comments to get an understanding of what you can do to wrap the response.

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;
    }
} 
Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • I think this approach is bad because it's handled on HTTP level and if you decide to use other serialization format like XML or perhaps protobuf it will break. I'd do such a thing somewhere else – Konrad Jun 02 '18 at 10:15
  • You have to deserialize the response every time. This kind of wrapping should happen before serialization – Konrad Jun 02 '18 at 10:16